Skip to content

Commit 7f4a2c4

Browse files
committedMay 20, 2010
webのリファクタ
1 parent 82e9542 commit 7f4a2c4

File tree

5 files changed

+364
-245
lines changed

5 files changed

+364
-245
lines changed
 

‎README

+37-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,45 @@ CloudForecast - server resource monitoring framework
22

33
WARNING: Alpha quality code
44

5-
# schedule daemon
6-
CF_DEBUG=1 ./cloudforecast_radar -c cloudforecast.yaml -l server_list.yaml
5+
サーバ等のリソース監視をするためのツールです。
6+
RRDToolの薄いラッパー、情報取得のためのフレームワークとして設計されています。
7+
CloudForecastは、4つのプロセスによって動作します。
78

8-
# fetcher worker
9-
CF_DEBUG=1 ./cf_fetcher_worker -c cloudforecast.yaml
9+
- 巡回デーモン
10+
- グラフ閲覧 HTTPD
11+
- 情報取得Gearmanワーカー
12+
- RRDファイル更新Gearmanワーカー
13+
14+
小規模な監視では、Gearmanがなくても動作可能です。
15+
動作イメージはdocsディレクトリ以下の cloudforecast.png になります
16+
17+
# 巡回デーモン
18+
$ ./cloudforecast_radar -r -c cloudforecast.yaml -l server_list.yaml
19+
- 起動すると5分ごとに巡回を行います
20+
- -r 再起動オプション。ライブラリや設定ファイルを更新すると自動で再起動します
21+
- -c 設定ファイル
22+
- -c サーバ一覧
1023

11-
# rrd update worker
12-
CF_DEBUG=1 ./cf_updater_worker -c cloudforecast.yaml
1324

1425
# web server
15-
CF_DEBUG=1 ./cloudforecast_web -p 5000 -c cloudforecast.yaml -l server_list.yaml
26+
$ ./cloudforecast_web -r- p 5000 -c cloudforecast.yaml -l server_list.yaml
27+
- グラフ閲覧 HTTPD
28+
- -p ポート httpdのport
29+
30+
31+
# 情報取得Gearmanワーカー
32+
$ ./cf_fetcher_worker -r -c cloudforecast.yaml \
33+
-max-workers 2 -max-request-per-child 100 -max-exection-time 60
34+
- geamarnでのリソース情報取得ワーカー
35+
- -max-worker preforkするワーカー数
36+
- -max-request-per-child 1ワーカープロセス処理回数。この回数を超えるとプロセスが新しく作り直される
37+
- -max-exection-time ワーカーの1回の取得作業でこれ以上の時間かかっている場合、そのワーカーを停止します
38+
39+
# RRDファイル更新Gearmanワーカー
40+
$ ./cf_updater_worker -r -c cloudforecast.yaml \
41+
-max-workers 2 -max-request-per-child 100 -max-exection-time 60
42+
 - gearmanでのリソース情報をrrdファイルに書き込むワーカー
43+
44+
#環境変数
45+
CF_DEBUG=1 をするとdebugログが出力されます
1646

‎cloudforecast_web

+17-159
Original file line numberDiff line numberDiff line change
@@ -3,171 +3,29 @@
33
use FindBin;
44
use lib "$FindBin::Bin/lib";
55
use lib "$FindBin::Bin/site-lib";
6-
use CloudForecast::Web -base;
7-
use CloudForecast::ConfigLoader;
8-
use CloudForecast::Host;
6+
use CloudForecast::Web::Server;
97
use Getopt::Long;
108

119
my $root_dir = $FindBin::Bin;
12-
my $config_yaml = $root_dir . '/cloudforecast.yaml';
13-
my $server_list_yaml = $root_dir . '/server_list.yaml';
10+
my $config = $root_dir . '/cloudforecast.yaml';
11+
my $server_list = $root_dir .'/server_list.yaml';
12+
my $restarter = 0;
13+
my $port = 5000;
1414

15-
my @argv = @ARGV;
16-
Getopt::Long::Configure("no_ignore_case", "pass_through");
1715
GetOptions(
18-
'c|config=s' => \$config_yaml,
19-
'l|server-list=s' => \$server_list_yaml,
16+
'port=s' => \$port,
17+
'r|restarter' => \$restarter,
18+
'c|config=s' => \$config,
19+
'l|server-list=s' => \$server_list,
2020
);
2121

22-
die 'config not found' unless $config_yaml;
23-
die 'server_list not found' unless $server_list_yaml;
22+
die 'config not found' unless $config;
23+
die 'server_list not found' unless $server_list;
2424

25-
my $configloader = CloudForecast::ConfigLoader->new({
25+
CloudForecast::Web::Server->new({
26+
port => $port,
27+
restarter => $restarter,
2628
root_dir => $root_dir,
27-
global_config => $config_yaml,
28-
server_list => $server_list_yaml,
29-
});
30-
$configloader->load_all();
31-
32-
my $global_config = $configloader->global_config;
33-
my $server_list = $configloader->server_list;
34-
my $all_hosts = $configloader->all_hosts;
35-
36-
my $page_title = $server_list_yaml;
37-
$page_title =~ s!^(.+)/!!;
38-
$page_title =~ s!\.[^.]+$!!;
39-
40-
sub get_host {
41-
my $host = shift;
42-
my $host_instance = CloudForecast::Host->new({
43-
address => $host->{address},
44-
hostname => $host->{hostname},
45-
details => $host->{details},
46-
resources => $host->{resources},
47-
component_config => $host->{component_config},
48-
global_config => $global_config,
49-
});
50-
$host_instance;
51-
}
52-
53-
get '/' => sub {
54-
my $req = shift;
55-
my $p = shift;
56-
return render('index.mt');
57-
};
58-
59-
get '/server' => sub {
60-
my $req = shift;
61-
62-
my $address = $req->param('address');
63-
return [ 404, [], ['Address Not Found'] ] unless $address;
64-
65-
my $host = $all_hosts->{$address};
66-
return [ 404, [], ['Host Not Found'] ] unless $host;
67-
68-
my $host_instance = get_host($host);
69-
my @graph_list = $host_instance->list_graph;
70-
71-
return render('server.mt');
72-
};
73-
74-
get '/graph' => sub {
75-
my $req = shift;
76-
77-
my $address = $req->param('address');
78-
return [ 404, [], ['Address Not Found'] ] unless $address;
79-
my $resource = $req->param('resource');
80-
return [ 404, [], ['Resource Not Found'] ] unless $resource;
81-
my $key = $req->param('key');
82-
return [ 404, [], ['Graph type key Not Found'] ] unless $key;
83-
84-
my $span = $req->param('span') || 'd';
85-
my $host = $all_hosts->{$address};
86-
return [ 404, [], ['Host Not Found'] ] unless $host;
87-
88-
my $host_instance = get_host($host);
89-
my ($img,$err) = $host_instance->draw_graph($resource,$key, $span);
90-
91-
return [ 500, [], ['Internal Server Error', $err] ] unless $img;
92-
return [ 200, ['Content-Type','image/png'], [$img] ];
93-
};
94-
95-
get '/default.css' => sub {
96-
my $req = shift;
97-
return [ 200, ['Content-Type','text/css'], [render('css.mt')] ];
98-
};
99-
100-
101-
run_server(@argv);
102-
103-
__DATA__
104-
@@ index.mt
105-
<html>
106-
<head>
107-
<title>CloudForecast Server List</title>
108-
<link rel="stylesheet" type="text/css" href="/default.css" />
109-
</head>
110-
<body>
111-
<h1 class="title"><?= $page_title ?> </h1>
112-
113-
<ul>
114-
<? my $i=0 ?>
115-
<? for my $server ( @$server_list ) { ?>
116-
<li><a href="#group-<?= $i ?>"><?= $server->{title} ?></a></li>
117-
<? $i++ } ?>
118-
</ul>
119-
120-
<hr>
121-
122-
<ul>
123-
<? my $k=0 ?>
124-
<? for my $server ( @$server_list ) { ?>
125-
<li id="group-<?= $k ?>"><?= $server->{title} ?></li>
126-
<ul>
127-
<? for my $host ( @{$server->{hosts}} ) { ?>
128-
<li><a href="/server?address=<?= $host->{address} ?>"><?= $host->{address} ?></a> <strong><?= $host->{hostname} ?></strong> <span class="details"><?= $host->{details} ?></a></li>
129-
<? } ?>
130-
</ul>
131-
<? $k++ } ?>
132-
</ul>
133-
134-
</body>
135-
</html>
136-
137-
@@ server.mt
138-
<html>
139-
<head>
140-
<title>CloudForecast Server List</title>
141-
<link rel="stylesheet" type="text/css" href="/default.css" />
142-
</head>
143-
<body>
144-
<h1 class="title"><?= $page_title ?> </h1>
145-
<h2><span class="address"><?= $host->{address} ?></span> <strong><?= $host->{hostname} ?></strong> <span class="details"><?= $host->{details} ?></a></h2>
146-
147-
<? for my $resource ( @graph_list ) { ?>
148-
<h4><?= $resource->{resource_class} ?></h4>
149-
<? for my $graph ( @{$resource->{graphs}} ) { ?>
150-
<nobr />
151-
<? for my $term ( qw/d w m y/ ) { ?>
152-
<img src="/graph?span=<?= $term ?>&amp;address=<?= $host->{address} ?>&amp;resource=<?= $resource->{resource} ?>&amp;key=<?= $graph ?>" />
153-
<? } ?>
154-
<br />
155-
<? } ?>
156-
<? } ?>
157-
158-
</body>
159-
</html>
160-
161-
162-
@@ css.mt
163-
164-
a { color: #5555cc;}
165-
a:link { color: #5555cc;}
166-
a:visited { color: #555599;}
167-
a:active { color: #999999; }
168-
a:hover { color: #999999; }
169-
170-
ol, ul{
171-
list-style-position:inside;
172-
}
173-
29+
global_config => $config,
30+
server_list => $server_list,
31+
})->run;

‎docs/cloudforecast.png

68.7 KB
Loading

‎lib/CloudForecast/Web.pm

+147-79
Original file line numberDiff line numberDiff line change
@@ -3,141 +3,209 @@ package CloudForecast::Web;
33
use strict;
44
use warnings;
55
use Carp qw//;
6+
use Encode qw//;
67
use Scalar::Util qw/refaddr/;
7-
use Plack::Runner;
8+
use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
9+
use Plack::Loader;
10+
use Plack::Builder;
811
use Plack::Request;
912
use Plack::Response;
1013
use Router::Simple;
1114
use Text::MicroTemplate;
1215
use Data::Section::Simple;
16+
use CloudForecast::Log;
17+
use CloudForecast::ConfigLoader;
1318

14-
my $_ROUTER;
15-
my %CACHE;
16-
our $KEY;
17-
our $DATA_SECTION_LEVEL = 0;
19+
__PACKAGE__->mk_classdata('_ROUTER');
20+
__PACKAGE__->mk_accessors(qw/configloader
21+
restarter
22+
port/);
1823

19-
our @EXPORT = qw/get post any render run_server/;
24+
our @EXPORT = qw/get post any/;
2025

2126
sub import {
2227
my ($class, $name) = @_;
2328
my $caller = caller;
2429
{
2530
no strict 'refs';
2631
if ( $name && $name =~ /^-base/ ) {
27-
28-
$_ROUTER = Router::Simple->new();
29-
30-
for my $func (@EXPORT) {
31-
*{"$caller\::$func"} = \&$func;
32+
if ( ! $caller->isa($class) && $caller ne 'main' ) {
33+
push @{"$caller\::ISA"}, $class;
3234
}
3335
}
36+
for my $func (@EXPORT) {
37+
*{"$caller\::$func"} = \&$func;
38+
}
3439
}
3540

3641
strict->import;
3742
warnings->import;
3843
}
3944

40-
sub psgify (&) {
41-
my $code = shift;
42-
sub {
43-
my $env = shift;
44-
my $req = Plack::Request->new($env);
45-
my $p = delete $env->{'cloudforecast-web.args'};
46-
my $res = $code->($req, $p);
47-
my $res_t = ref $res || '';
48-
if ( $res_t eq 'Plack::Response' ) {
49-
return $res->finalize;
50-
}
51-
elsif ( $res_t eq 'ARRAY' ) {
52-
return $res;
53-
}
54-
elsif ( !$res_t ) {
55-
return [ 200, [ 'Content-Type' => 'text/html; charset=utf-8'], [ $res ] ];
56-
}
57-
else {
58-
Carp::croak("unknown response type: $res, $res_t");
59-
}
45+
sub new {
46+
my $class = shift;
47+
my $args = ref $_[0] ? shift : { @_ };
48+
49+
my $configloader = CloudForecast::ConfigLoader->new({
50+
root_dir => $args->{root_dir},
51+
global_config => $args->{global_config},
52+
server_list => $args->{server_list},
53+
});
54+
$configloader->load_all();
55+
56+
$class->SUPER::new({
57+
configloader => $configloader,
58+
restarter => $args->{restarter},
59+
port => $args->{port} || 5000,
60+
});
61+
}
62+
63+
sub run {
64+
my $self = shift;
65+
66+
my $app = $self->build_app;
67+
$app = builder {
68+
enable 'Plack::Middleware::Lint';
69+
enable 'Plack::Middleware::StackTrace';
70+
$app;
6071
};
72+
73+
74+
my $loader = Plack::Loader->load(
75+
'Starlet',
76+
port => $self->port || 5000,
77+
max_workers => 2,
78+
);
79+
80+
my @watchdog_pid;
81+
if ( $self->restarter ) {
82+
CloudForecast::Log->debug("restarter start");
83+
push @watchdog_pid, $self->configloader->watchdog;
84+
}
85+
86+
$loader->run($app);
87+
88+
for my $pid ( @watchdog_pid ) {
89+
kill 'TERM', $pid;
90+
waitpid( $pid, 0 );
91+
}
6192
}
6293

63-
sub router_to_app {
64-
my $router = shift;
94+
sub build_app {
95+
my $self = shift;
6596
sub {
66-
if ( my $p = $router->match($_[0]) ) {
97+
my $env = shift;
98+
if ( my $p = $self->router->match($env) ) {
6799
my $code = delete $p->{action};
68-
return [ 500, [], ['Internal Server Error'] ] unless $code;
69-
$_[0]->{'cloudforecast-web.args'} = $p;
70-
return $code->(@_)
71-
}
72-
else {
73-
return [ 404, [ 'Content-Type' => 'text/html; charset=utf-8' ], ['not found'] ];
100+
return $self->ise('uri match but no action found') unless $code;
101+
102+
my $req = Plack::Request->new($env);
103+
my $res = $code->($self, $req, $p);
104+
105+
my $res_t = ref $res || '';
106+
if ( $res_t eq 'Plack::Response' ) {
107+
return $res->finalize;
108+
}
109+
elsif ( $res_t eq 'ARRAY' ) {
110+
return $res;
111+
}
112+
elsif ( !$res_t ) {
113+
return $self->html_response( $res );
114+
}
115+
else {
116+
Carp::croak("unknown response type: $res, $res_t");
117+
}
74118
}
75-
}
119+
return $self->not_found();
120+
};
121+
}
122+
123+
sub ise {
124+
my $self = shift;
125+
my $error = shift;
126+
CloudForecast::Log->warn($error) if $error;
127+
$error ||= 'Internal Server Error';
128+
return [ 500, [ 'Content-Type' => 'text/html; charset=utf-8' ], [$error] ];
129+
}
130+
131+
sub not_found {
132+
my $self = shift;
133+
my $error = shift;
134+
CloudForecast::Log->warn($error) if $error;
135+
$error ||= 'Not Found';
136+
return [ 404, [ 'Content-Type' => 'text/html; charset=utf-8' ], [$error] ];
76137
}
77138

78-
sub run_server {
79-
my $runner = Plack::Runner->new;
80-
$runner->parse_options(@_);
139+
sub html_response {
140+
my $self = shift;
141+
my $message = shift;
142+
return [ 200, [ 'Content-Type' => 'text/html; charset=utf-8'], [ $message ] ];
143+
}
81144

82-
my $router_app = router_to_app($_ROUTER);
83-
my $app = sub {
84-
local $KEY = refaddr $router_app;
85-
$router_app->(@_);
145+
sub render {
146+
my ( $self, $key, @args ) = @_;
147+
my $code = do {
148+
my $reader = Data::Section::Simple->new(ref $self);
149+
my $tmpl = $reader->get_data_section($key);
150+
Carp::croak("unknown template file:$key") unless $tmpl;
151+
Text::MicroTemplate->new(template => $tmpl, package_name => ref($self) )->code();
86152
};
87153

88-
$runner->run($app);
154+
package DB;
155+
local *DB::render = sub {
156+
my $coderef = (eval $code); ## no critic
157+
die "Cannot compile template '$key': $@" if $@;
158+
my $html = $coderef->(@args);
159+
$html = Encode::encode_utf8($html) if utf8::is_utf8($html);
160+
$html;
161+
};
162+
goto &DB::render;
89163
}
90164

91165

92-
sub any($$;$) {
166+
sub router {
167+
my $class = shift;
168+
my $router = $class->_ROUTER;
169+
if ( !$router ) {
170+
$router = $class->_ROUTER( Router::Simple->new() );
171+
}
172+
$router;
173+
}
174+
175+
sub _any($$$;$) {
176+
my $class = shift;
93177
if ( @_ == 3 ) {
94178
my ( $methods, $pattern, $code ) = @_;
95-
$_ROUTER->connect(
179+
$class->router->connect(
96180
$pattern,
97-
{ action => psgify { goto $code } },
181+
{ action => $code },
98182
{ method => [ map { uc $_ } @$methods ] }
99183
);
100184
}
101185
else {
102186
my ( $pattern, $code ) = @_;
103-
$_ROUTER->connect(
187+
$class->router->connect(
104188
$pattern,
105-
{ action => psgify { goto $code } }
189+
{ action => $code }
106190
);
107191
}
108192
}
109193

110-
sub get {
111-
any( ['GET','HEAD'], $_[0], $_[1] );
194+
sub any {
195+
my $class = caller;
196+
$class->_any( @_ );
112197
}
113198

114-
sub post {
115-
any( ['POST'], $_[0], $_[1] );
199+
sub get {
200+
my $class = caller;
201+
$class->_any( ['GET','HEAD'], $_[0], $_[1] );
116202
}
117203

118-
sub get_data_section {
119-
my $pkg = caller($DATA_SECTION_LEVEL);
120-
my $data = $CACHE{$KEY}->{__data_section} ||= Data::Section::Simple->new($pkg)->get_data_section;
121-
return @_ ? $data->{$_[0]} : $data;
204+
sub post {
205+
my $class = caller;
206+
$class->_any( ['POST'], $_[0], $_[1] );
122207
}
123208

124-
sub render {
125-
my ( $key, @args ) = @_;
126-
my $code = $CACHE{$KEY}->{$key} ||= do {
127-
local $DATA_SECTION_LEVEL = $DATA_SECTION_LEVEL + 1;
128-
my $tmpl = get_data_section($key);
129-
Carp::croak("unknown template file:$key") unless $tmpl;
130-
Text::MicroTemplate->new(template => $tmpl, package_name => 'main')->code();
131-
};
132-
133-
package DB;
134-
local *DB::render = sub {
135-
my $coderef = (eval $code); ## no critic
136-
die "Cannot compile template '$key': $@" if $@;
137-
$coderef->(@args);
138-
};
139-
goto &DB::render;
140-
}
141209

142210
1;
143211

‎lib/CloudForecast/Web/Server.pm

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package CloudForecast::Web::Server;
2+
3+
use strict;
4+
use warnings;
5+
use CloudForecast::Web -base;
6+
use CloudForecast::Host;
7+
8+
sub get_host {
9+
my ( $self, $host ) = @_;
10+
my $host_instance = CloudForecast::Host->new({
11+
address => $host->{address},
12+
hostname => $host->{hostname},
13+
details => $host->{details},
14+
resources => $host->{resources},
15+
component_config => $host->{component_config},
16+
global_config => $self->configloader->global_config,
17+
});
18+
$host_instance;
19+
}
20+
21+
sub page_title {
22+
my $self = shift;
23+
my $page_title = $self->configloader->server_list_yaml;
24+
$page_title =~ s!^(.+)/!!;
25+
$page_title =~ s!\.[^.]+$!!;
26+
$page_title;
27+
}
28+
29+
# shortcut
30+
sub all_hosts {
31+
my $self = shift;
32+
$self->configloader->all_hosts;
33+
}
34+
35+
sub server_list {
36+
my $self = shift;
37+
$self->configloader->server_list;
38+
}
39+
40+
get '/' => sub {
41+
my ( $self, $req, $p ) = @_;
42+
return $self->render('index.mt');
43+
};
44+
45+
get '/server' => sub {
46+
my ($self, $req, $p ) = @_;
47+
48+
my $address = $req->param('address');
49+
return $self->not_found('Address Not Found') unless $address;
50+
51+
my $host = $self->all_hosts->{$address};
52+
return $self->not_found('Host Not Found') unless $host;
53+
54+
my $host_instance = $self->get_host($host);
55+
my @graph_list = $host_instance->list_graph;
56+
57+
return $self->render('server.mt');
58+
};
59+
60+
61+
get '/graph' => sub {
62+
my ($self, $req ) = @_;
63+
64+
my $address = $req->param('address');
65+
return $self->not_found('Address Not Found') unless $address;
66+
my $resource = $req->param('resource');
67+
return $self->not_found('Resource Not Found') unless $resource;
68+
my $key = $req->param('key');
69+
return $self->not_found('Graph type key Not Found') unless $key;
70+
71+
my $span = $req->param('span') || 'd';
72+
my $host = $self->all_hosts->{$address};
73+
return $self->not_found('Host Not Found') unless $host;
74+
75+
my $host_instance = $self->get_host($host);
76+
my ($img,$err) = $host_instance->draw_graph($resource,$key, $span);
77+
78+
return $self->ise($err) unless $img;
79+
return [ 200, ['Content-Type','image/png'], [$img] ];
80+
};
81+
82+
get '/default.css' => sub {
83+
my ($self, $req) = @_;
84+
return [ 200, ['Content-Type','text/css'], [ $self->render('css.mt')] ];
85+
};
86+
87+
88+
1;
89+
90+
__DATA__
91+
@@ index.mt
92+
<html>
93+
<head>
94+
<title>CloudForecast Server List</title>
95+
<link rel="stylesheet" type="text/css" href="/default.css" />
96+
</head>
97+
<body>
98+
<h1 class="title">CloudForecast : <?= $self->page_title ?> </h1>
99+
100+
<ul>
101+
<? my $i=0 ?>
102+
<? for my $server ( @{$self->server_list} ) { ?>
103+
<li><a href="#group-<?= $i ?>"><?= $server->{title} ?></a></li>
104+
<? $i++ } ?>
105+
</ul>
106+
107+
<hr>
108+
109+
<ul>
110+
<? my $k=0 ?>
111+
<? for my $server ( @{$self->server_list} ) { ?>
112+
<li id="group-<?= $k ?>"><?= $server->{title} ?></li>
113+
<ul>
114+
<? for my $host ( @{$server->{hosts}} ) { ?>
115+
<li><a href="/server?address=<?= $host->{address} ?>"><?= $host->{address} ?></a> <strong><?= $host->{hostname} ?></strong> <span class="details"><?= $host->{details} ?></a></li>
116+
<? } ?>
117+
</ul>
118+
<? $k++ } ?>
119+
</ul>
120+
121+
</body>
122+
</html>
123+
124+
@@ server.mt
125+
<html>
126+
<head>
127+
<title>CloudForecast : <?= $self->page_title ?> : <?= $host->{address} ?></title>
128+
<link rel="stylesheet" type="text/css" href="/default.css" />
129+
</head>
130+
<body>
131+
<h1 class="title">CloudForecast : <?= $self->page_title ?></h1>
132+
<h2><span class="address"><?= $host->{address} ?></span> <strong><?= $host->{hostname} ?></strong> <span class="details"><?= $host->{details} ?></a></h2>
133+
134+
<? for my $resource ( @graph_list ) { ?>
135+
<h4><?= $resource->{resource_class} ?></h4>
136+
<? for my $graph ( @{$resource->{graphs}} ) { ?>
137+
<nobr />
138+
<? for my $term ( qw/d w m y/ ) { ?>
139+
<img src="/graph?span=<?= $term ?>&amp;address=<?= $host->{address} ?>&amp;resource=<?= $resource->{resource} ?>&amp;key=<?= $graph ?>" />
140+
<? } ?>
141+
<br />
142+
<? } ?>
143+
<? } ?>
144+
145+
</body>
146+
</html>
147+
148+
149+
@@ css.mt
150+
151+
a { color: #5555cc;}
152+
a:link { color: #5555cc;}
153+
a:visited { color: #555599;}
154+
a:active { color: #999999; }
155+
a:hover { color: #999999; }
156+
157+
ol, ul{
158+
list-style-position:inside;
159+
}
160+
161+
162+
163+

0 commit comments

Comments
 (0)
Please sign in to comment.