diff --git a/lib/Dobby/Boxmate/App/Command.pm b/lib/Dobby/Boxmate/App/Command.pm index 04a48a2..a5c7965 100644 --- a/lib/Dobby/Boxmate/App/Command.pm +++ b/lib/Dobby/Boxmate/App/Command.pm @@ -9,7 +9,7 @@ sub boxman ($self) { $self->app->boxman; } -sub droplet_from_prefix ($self, $boxprefix) { +sub maybe_droplet_from_prefix ($self, $boxprefix) { length $boxprefix || $self->usage->die({ pre_text => "No box prefix provided.\n\n" }); @@ -21,6 +21,12 @@ sub droplet_from_prefix ($self, $boxprefix) { my $want_name = join q{.}, $boxprefix, $username, $boxman->box_domain; my ($droplet) = grep {; $_->{name} eq $want_name } @$droplets; + return $droplet; +} + +sub droplet_from_prefix ($self, $boxprefix) { + my $droplet = $self->maybe_droplet_from_prefix($boxprefix); + unless ($droplet) { die "Couldn't find box for $boxprefix\n"; } @@ -28,4 +34,77 @@ sub droplet_from_prefix ($self, $boxprefix) { return $droplet; } +sub print_droplet_list ($self, $droplets, $username = undef) { + require DateTime::Format::RFC3339; + require Term::ANSIColor; + require Text::Table; + require Time::Duration; + + my $parser = DateTime::Format::RFC3339->new; + my $boxman = $self->boxman; + + # Ugh, should sort out the ME:: table formatter for general use. + my $table = Text::Table->new( + '', # Status + 'region', + ' ', # Type + ' ', # Default + 'name', + 'ip', + { title => 'age', align => 'right' }, + { title => 'cost', align => 'right' }, + { title => 'img age', align => 'right', align_title => 'right' }, + ); + + my $default; + if ($username) { + my ($rec) = grep {; $_->{type} eq 'CNAME' && $_->{name} eq $username } + $boxman->dobby->get_all_domain_records_for_domain($boxman->box_domain)->get; + + $default = $rec->{data}; + } + + for my $droplet (@$droplets) { + my $name = $droplet->{name}; + my $status = $droplet->{status}; + my $ip = $boxman->_ip_address_for_droplet($droplet); # XXX _method + my $image = $droplet->{image}; + + my $created = $parser->parse_datetime($droplet->{created_at}); + my $age_secs = time - $created->epoch; + + my $img_created = $parser->parse_datetime($image->{created_at}); + my $img_age_secs = time - $img_created->epoch; + + my $cost = sprintf '%4s', + '$' . builtin::ceil($droplet->{size}{price_hourly} * $age_secs / 3600); + + my $icon = ($image->{slug} && $image->{slug} =~ /^debian/) ? "\N{CYCLONE}" + : (($image->{description}//'') =~ /^Debian/) ? "\N{CYCLONE}" # Deb 11 + : ($image->{name} =~ /\Afminabox/ ) ? "\N{PACKAGE}" + : "\N{BLACK QUESTION MARK ORNAMENT}"; + + my $default = $default && $default eq $name + ? "\N{SPARKLES}" + : "\N{IDEOGRAPHIC SPACE}"; + + $table->add( + ($status eq 'active' ? "\N{LARGE GREEN CIRCLE}" : "\N{HEAVY MINUS SIGN}"), + $droplet->{region}{slug}, + "$icon\N{INVISIBLE SEPARATOR}", + "$default\N{INVISIBLE SEPARATOR}", + $name, + $ip, + Time::Duration::concise(Time::Duration::duration($age_secs, 1)), + $cost, + Time::Duration::concise(Time::Duration::duration($img_age_secs, 1)), + ); + } + + # This leading space is *bananas* and is here because Text::Table will think + # about LARGE GREEN CIRCLE as being one wide, but it's two. + print Term::ANSIColor::colored(['bold', 'bright_white'], qq{ $_}) for $table->title; + print qq{$_} for $table->body; +} + 1; diff --git a/lib/Dobby/Boxmate/App/Command/destroy.pm b/lib/Dobby/Boxmate/App/Command/destroy.pm index 2278819..14c808b 100644 --- a/lib/Dobby/Boxmate/App/Command/destroy.pm +++ b/lib/Dobby/Boxmate/App/Command/destroy.pm @@ -8,6 +8,21 @@ use utf8; sub abstract { 'destroy a box' } +sub opt_spec { + return ( + [ finder => hidden => { + default => 'by_prefix', + one_of => [ + [ 'by-prefix' => 'find box by prefix of default domain' ], + [ 'by-id|I' => 'find box by Droplet id' ], + [ 'by-name|N' => 'find box by Droplet name' ], + ], + } ], + [], + [ 'ip=s', 'only destroy if the public IP is this' ], + ); +} + sub usage_desc { '%c destroy %o BOXPREFIX', } @@ -18,9 +33,44 @@ sub validate_args ($self, $opt, $args) { sub execute ($self, $opt, $args) { my $boxman = $self->boxman; - my $droplet = $self->droplet_from_prefix($args->[0]); + my $locator = $args->[0]; + + my $method = "_find_" . $opt->finder; + + my @droplets = $self->$method($opt, $locator); + + if ($opt->ip) { + @droplets = grep { + $opt->ip eq $self->boxman->_ip_address_for_droplet($_) # XXX _method + } @droplets; + } + + unless (@droplets) { + die "I couldn't find the box you want to destroy.\n"; + } + + if (@droplets > 1) { + $self->print_droplet_list(\@droplets, undef); + say ""; + + die "More than one box matched your criteria.\n"; + } + + $boxman->destroy_droplet($droplets[0], { force => 1 })->get; +} + +sub _find_by_prefix ($self, $opt, $locator) { + return $self->maybe_droplet_from_prefix($locator); +} + +sub _find_by_id ($self, $opt, $locator) { + $opt->ip || die "You can't destroy a box by id without --ip for safety.\n"; + return $self->boxman->dobby->get_droplet_by_id($locator)->get; +} - $boxman->destroy_droplet($droplet, { force => 1 })->get; +sub _find_by_name ($self, $opt, $locator) { + my @droplets = $self->boxman->dobby->get_droplets_by_name($locator)->get; + return @droplets; } 1; diff --git a/lib/Dobby/Boxmate/App/Command/list.pm b/lib/Dobby/Boxmate/App/Command/list.pm index 86e131c..7d1f0bc 100644 --- a/lib/Dobby/Boxmate/App/Command/list.pm +++ b/lib/Dobby/Boxmate/App/Command/list.pm @@ -58,75 +58,8 @@ sub execute ($self, $opt, $args) { return; } - require DateTime::Format::RFC3339; - require Term::ANSIColor; - require Text::Table; - require Time::Duration; - - my $parser = DateTime::Format::RFC3339->new; - - # Ugh, should sort out the ME:: table formatter for general use. - my $table = Text::Table->new( - '', # Status - 'region', - ' ', # Type - ' ', # Default - 'name', - 'ip', - { title => 'age', align => 'right' }, - { title => 'cost', align => 'right' }, - { title => 'img age', align => 'right', align_title => 'right' }, - ); - - my $default; - unless ($opt->everything) { - my ($rec) = grep {; $_->{type} eq 'CNAME' && $_->{name} eq $username } - $boxman->dobby->get_all_domain_records_for_domain($boxman->box_domain)->get; - - $default = $rec->{data}; - } - - for my $droplet (@$droplets) { - my $name = $droplet->{name}; - my $status = $droplet->{status}; - my $ip = $self->boxman->_ip_address_for_droplet($droplet); # XXX _method - my $image = $droplet->{image}; - - my $created = $parser->parse_datetime($droplet->{created_at}); - my $age_secs = time - $created->epoch; - - my $img_created = $parser->parse_datetime($image->{created_at}); - my $img_age_secs = time - $img_created->epoch; - - my $cost = sprintf '%4s', - '$' . builtin::ceil($droplet->{size}{price_hourly} * $age_secs / 3600); - - my $icon = ($image->{slug} && $image->{slug} =~ /^debian/) ? "\N{CYCLONE}" - : (($image->{description}//'') =~ /^Debian/) ? "\N{CYCLONE}" # Deb 11 - : ($image->{name} =~ /\Afminabox/ ) ? "\N{PACKAGE}" - : "\N{BLACK QUESTION MARK ORNAMENT}"; - - my $default = $default && $default eq $name - ? "\N{SPARKLES}" - : "\N{IDEOGRAPHIC SPACE}"; - - $table->add( - ($status eq 'active' ? "\N{LARGE GREEN CIRCLE}" : "\N{HEAVY MINUS SIGN}"), - $droplet->{region}{slug}, - "$icon\N{INVISIBLE SEPARATOR}", - "$default\N{INVISIBLE SEPARATOR}", - $name, - $ip, - Time::Duration::concise(Time::Duration::duration($age_secs, 1)), - $cost, - Time::Duration::concise(Time::Duration::duration($img_age_secs, 1)), - ); - } - - # This leading space is *bananas* and is here because Text::Table will think - # about LARGE GREEN CIRCLE as being one wide, but it's two. - print Term::ANSIColor::colored(['bold', 'bright_white'], qq{ $_}) for $table->title; - print qq{$_} for $table->body; + $self->print_droplet_list($droplets, $username); + return; } 1; diff --git a/lib/Dobby/Client.pm b/lib/Dobby/Client.pm index 40b295c..5be51ae 100644 --- a/lib/Dobby/Client.pm +++ b/lib/Dobby/Client.pm @@ -50,7 +50,9 @@ sub http ($self) { }; } -async sub json_get ($self, $path) { +async sub json_get ($self, $path, $arg=undef) { + my $undef_if_404 = $arg && $arg->{undef_if_404}; + my $res = await $self->http->do_request( method => 'GET', uri => $self->api_base . $path, @@ -60,6 +62,10 @@ async sub json_get ($self, $path) { ); unless ($res->is_success) { + if ($undef_if_404 && $res->code == 404) { + return undef; + } + die "error getting $path at DigitalOcean: " . $res->as_string; } @@ -247,6 +253,7 @@ async sub _do_action_status_f ($self, $action_url) { async sub _get_droplets ($self, $arg = {}) { my $path = '/droplets?per_page=200'; $path .= "&tag_name=$arg->{tag}" if $arg->{tag}; + $path .= "&name=$arg->{name}" if $arg->{name}; my $droplets_data = await $self->json_get($path); @@ -272,6 +279,25 @@ async sub get_droplets_with_tag ($self, $tag) { await $self->_get_droplets({ tag => $tag }); } +async sub get_droplet_by_id ($self, $id) { + $id =~ /\A[0-9]+\z/ + || Carp::croak("bogus id given to get_droplet_by_id; should be a string of digits"); + + my $path = "/droplets/$id"; + + my $droplet = await $self->json_get($path, { undef_if_404 => 1 }); + + return $droplet; +} + +async sub get_droplets_by_name ($self, $name) { + length $name + || Carp::croak("get_droplet_by_name without a name passed in"); + + my @droplets = await $self->_get_droplets({ name => $name }); + return @droplets; +} + async sub add_droplet_to_project ($self, $droplet_id, $project_id) { my $path = "/projects/$project_id/resources";