Commit 2e9ea066 authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Merge branch 'feature-network-api' into develop

parents e3f54dc0 8ac8898f
......@@ -52,6 +52,7 @@ class Argument(object):
This is the top-level Argument class. It is suggested to extent this
class into more specific argument types.
"""
lvalue_delimiter = '/'
def __init__(self, arity, help=None, parsed_name=None, default=None):
self.arity = int(arity)
......@@ -86,6 +87,12 @@ class Argument(object):
*self.parsed_name,
dest=name, action=action, default=self.default, help=self.help)
@property
def lvalue(self):
"""A printable form of the left value when calling an argument e.g.,
--left-value=right-value"""
return (self.lvalue_delimiter or ' ').join(self.parsed_name or [])
class ConfigArgument(Argument):
"""Manage a kamaki configuration (file)"""
......
......@@ -263,7 +263,7 @@ class OutputFormatArgument(ValueArgument):
else:
raise CLIInvalidArgument(
'Invalid value %s for argument %s' % (
newvalue, '/'.join(self.parsed_name)),
newvalue, self.lvalue),
details=['Valid output formats: %s' % ', '.join(self.formats)])
......
......@@ -52,9 +52,7 @@ from kamaki.cli.commands import (
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
_commands = [server_cmds, flavor_cmds, network_cmds]
_commands = [server_cmds, flavor_cmds]
about_authentication = '\nUser Authentication:\
......@@ -415,13 +413,19 @@ class server_create(_init_cyclades, _optional_json, _server_wait):
'Connect server to network w. floating ip ( NETWORK_ID,IP )'
'(can be repeated)',
'--network-with-ip'),
automatic_ip=FlagArgument(
'Automatically assign an IP to the server', '--automatic-ip')
)
required = ('server_name', 'flavor_id', 'image_id')
@errors.cyclades.cluster_size
def _create_cluster(self, prefix, flavor_id, image_id, size):
networks = [dict(network=netid) for netid in (
self['network_id'] or [])] + (self['network_id_and_ip'] or [])
if self['automatic_ip']:
networks = []
else:
networks = [dict(network=netid) for netid in (
(self['network_id'] or []) + (self['network_id_and_ip'] or [])
)] or None
servers = [dict(
name='%s%s' % (prefix, i if size > 1 else ''),
flavor_id=flavor_id,
......@@ -473,6 +477,14 @@ class server_create(_init_cyclades, _optional_json, _server_wait):
def main(self):
super(self.__class__, self)._run()
if self['automatic_ip'] and (
self['network_id'] or self['network_id_and_ip']):
raise CLIInvalidArgument('Invalid argument combination', details=[
'Argument %s should not be combined with other' % (
self.arguments['automatic_ip'].lvalue),
'network-related arguments i.e., %s or %s' % (
self.arguments['network_id'].lvalue,
self.arguments['network_id_and_ip'].lvalue)])
self._run(
name=self['server_name'],
flavor_id=self['flavor_id'],
......@@ -865,30 +877,3 @@ def _add_name(self, net):
net['user_id'] += ' (%s)' % usernames[user_id]
if tenant_id:
net['tenant_id'] += ' (%s)' % usernames[tenant_id]
@command(network_cmds)
class network_wait(_init_cyclades, _network_wait):
"""Wait for server to finish [PENDING, ACTIVE, DELETED]"""
arguments = dict(
timeout=IntArgument(
'Wait limit in seconds (default: 60)', '--timeout', default=60)
)
@errors.generic.all
@errors.cyclades.connection
@errors.cyclades.network_id
def _run(self, network_id, current_status):
net = self.client.get_network_details(network_id)
if net['status'].lower() == current_status.lower():
self._wait(network_id, current_status, timeout=self['timeout'])
else:
self.error(
'Network %s: Cannot wait for status %s, '
'status is already %s' % (
network_id, current_status, net['status']))
def main(self, network_id, current_status='PENDING'):
super(self.__class__, self)._run()
self._run(network_id=network_id, current_status=current_status)
......@@ -39,11 +39,13 @@ from kamaki.cli.command_tree import CommandTree
from kamaki.cli.errors import (
CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
from kamaki.clients.cyclades import CycladesNetworkClient
from kamaki.cli.argument import FlagArgument, ValueArgument, RepeatableArgument
from kamaki.cli.argument import (
FlagArgument, ValueArgument, RepeatableArgument, IntArgument)
from kamaki.cli.commands import _command_init, errors, addLogSettings
from kamaki.cli.commands import (
_optional_output_cmd, _optional_json, _name_filter, _id_filter)
from kamaki.cli.utils import filter_dicts_by_dict
from kamaki.cli.commands.cyclades import _service_wait
network_cmds = CommandTree('network', 'Networking API network commands')
......@@ -58,6 +60,30 @@ about_authentication = '\nUser Authentication:\
\n* to set authentication token: /config set cloud.<cloud>.token <token>'
class _network_wait(_service_wait):
def _wait(self, net_id, current_status, timeout=60):
super(_network_wait, self)._wait(
'Network', net_id, self.client.wait_network, current_status,
timeout=timeout)
class _port_wait(_service_wait):
def _wait(self, port_id, current_status, timeout=60):
super(_port_wait, self)._wait(
'Port', port_id, self.client.wait_port, current_status,
timeout=timeout)
class _port_wait(_service_wait):
def _wait(self, net_id, current_status, timeout=60):
super(_network_wait, self)._wait(
'Network', net_id, self.client.wait_network, current_status,
timeout=timeout)
class _init_network(_command_init):
@errors.generic.all
@addLogSettings
......@@ -164,7 +190,7 @@ class NetworkTypeArgument(ValueArgument):
@command(network_cmds)
class network_create(_init_network, _optional_json):
class network_create(_init_network, _optional_json, _network_wait):
"""Create a new network"""
arguments = dict(
......@@ -173,7 +199,8 @@ class network_create(_init_network, _optional_json):
'Make network shared (special privileges required)', '--shared'),
network_type=NetworkTypeArgument(
'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
'--type')
'--type'),
wait=FlagArgument('Wait network to build', ('-w', '--wait')),
)
required = ('network_type', )
......@@ -183,6 +210,8 @@ class network_create(_init_network, _optional_json):
def _run(self, network_type):
net = self.client.create_network(
network_type, name=self['name'], shared=self['shared'])
if self['wait']:
self._wait(net['id'], net['status'])
self._print(net, self.print_dict)
def main(self):
......@@ -225,6 +254,33 @@ class network_modify(_init_network, _optional_json):
self._run(network_id=network_id)
@command(network_cmds)
class network_wait(_init_network, _network_wait):
"""Wait for network to finish [PENDING, ACTIVE, DELETED]"""
arguments = dict(
timeout=IntArgument(
'Wait limit in seconds (default: 60)', '--timeout', default=60)
)
@errors.generic.all
@errors.cyclades.connection
@errors.cyclades.network_id
def _run(self, network_id, current_status):
net = self.client.get_network_details(network_id)
if net['status'].lower() == current_status.lower():
self._wait(network_id, current_status, timeout=self['timeout'])
else:
self.error(
'Network %s: Cannot wait for status %s, '
'status is already %s' % (
network_id, current_status, net['status']))
def main(self, network_id, current_status='PENDING'):
super(self.__class__, self)._run()
self._run(network_id=network_id, current_status=current_status)
@command(subnet_cmds)
class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
"""List subnets
......@@ -432,7 +488,7 @@ class port_modify(_init_network, _optional_json):
@command(port_cmds)
class port_create(_init_network, _optional_json):
class port_create(_init_network, _optional_json, _port_wait):
"""Create a new port (== connect server to network)"""
arguments = dict(
......@@ -448,7 +504,8 @@ class port_create(_init_network, _optional_json):
network_id=ValueArgument('Set the network ID', '--network-id'),
device_id=ValueArgument(
'The device is either a virtual server or a virtual router',
'--device-id')
'--device-id'),
wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
)
required = ('network_id', 'device_id')
......@@ -464,6 +521,8 @@ class port_create(_init_network, _optional_json):
name=self['name'],
security_groups=self['security_group_id'],
fixed_ips=fixed_ips)
if self['wait']:
self._wait(r['id'], r['status'])
self._print(r, self.print_dict)
def main(self):
......@@ -471,6 +530,32 @@ class port_create(_init_network, _optional_json):
self._run(network_id=self['network_id'], device_id=self['device_id'])
@command(port_cmds)
class port_wait(_init_network, _port_wait):
"""Wait for port to finish [PENDING, ACTIVE, DELETED]"""
arguments = dict(
timeout=IntArgument(
'Wait limit in seconds (default: 60)', '--timeout', default=60)
)
@errors.generic.all
@errors.cyclades.connection
def _run(self, port_id, current_status):
port = self.client.get_port_details(port_id)
if port['status'].lower() == current_status.lower():
self._wait(port_id, current_status, timeout=self['timeout'])
else:
self.error(
'Port %s: Cannot wait for status %s, '
'status is already %s' % (
port_id, current_status, port['status']))
def main(self, port_id, current_status='PENDING'):
super(self.__class__, self)._run()
self._run(port_id=port_id, current_status=current_status)
@command(ip_cmds)
class ip_list(_init_network, _optional_json):
"""List reserved floating IPs"""
......@@ -492,7 +577,8 @@ class ip_info(_init_network, _optional_json):
@errors.generic.all
@errors.cyclades.connection
def _run(self, ip_id):
self._print(self.client.get_floatingip_details(ip_id))
self._print(
self.client.get_floatingip_details(ip_id), self.print_dict)
def main(self, ip_id):
super(self.__class__, self)._run()
......@@ -529,7 +615,7 @@ class ip_delete(_init_network, _optional_output_cmd):
"""Unreserve an IP (also delete the port, if attached)"""
def _run(self, ip_id):
self._optional_output(self.client.floatingip_delete(ip_id))
self._optional_output(self.client.delete_floatingip(ip_id))
def main(self, ip_id):
super(self.__class__, self)._run()
......
......@@ -377,14 +377,14 @@ class file_modify(_pithos_container):
if self['publish'] and self['unpublish']:
raise CLIInvalidArgument(
'Arguments %s and %s cannot be used together' % (
'/'.join(self.arguments['publish'].parsed_name),
'/'.join(self.arguments['publish'].parsed_name)))
self.arguments['publish'].lvalue,
self.arguments['publish'].lvalue))
if self['no_permissions'] and (
self['uuid_for_read_permission'] or self[
'uuid_for_write_permission']):
raise CLIInvalidArgument(
'%s cannot be used with other permission arguments' % '/'.join(
self.arguments['no_permissions'].parsed_name))
'%s cannot be used with other permission arguments' % (
self.arguments['no_permissions'].lvalue))
self._run()
......@@ -555,8 +555,8 @@ class _source_destination(_pithos_container, _optional_output_cmd):
self.dst_client.account,
self.dst_client.container,
dst_path),
'Use %s to transfer overwrite' % ('/'.join(
self.arguments['force'].parsed_name))])
'Use %s to transfer overwrite' % (
self.arguments['force'].lvalue)])
else:
# One object transfer
try:
......@@ -570,8 +570,7 @@ class _source_destination(_pithos_container, _optional_output_cmd):
'Missing specific path container %s' % self.container,
importance=2, details=[
'To transfer container contents %s' % (
'/'.join(self.arguments[
'source_prefix'].parsed_name))])
self.arguments['source_prefix'].lvalue)])
raise
dst_path = self.dst_path or self.path
dst_obj = dst_objects.get(dst_path or self.path, None)
......@@ -589,8 +588,7 @@ class _source_destination(_pithos_container, _optional_output_cmd):
self.container,
self.path),
'To recursively copy a directory, use',
' %s' % ('/'.join(
self.arguments['source_prefix'].parsed_name)),
' %s' % self.arguments['source_prefix'].lvalue,
'To create a file, use',
' /file create (general purpose)',
' /file mkdir (a directory object)'])
......@@ -607,8 +605,8 @@ class _source_destination(_pithos_container, _optional_output_cmd):
self.dst_client.account,
self.dst_client.container,
dst_path),
'Use %s to transfer overwrite' % ('/'.join(
self.arguments['force'].parsed_name))])
'Use %s to transfer overwrite' % (
self.arguments['force'].lvalue)])
return pairs
def _run(self, source_path_or_url, destination_path_or_url=''):
......@@ -873,8 +871,8 @@ class file_upload(_pithos_container, _optional_output_cmd):
if path.isdir(lpath):
if not self['recursive']:
raise CLIError('%s is a directory' % lpath, details=[
'Use %s to upload directories & contents' % '/'.join(
self.arguments['recursive'].parsed_name)])
'Use %s to upload directories & contents' % (
self.arguments['recursive'].lvalue)])
robj = self.client.container_get(path=rpath)
if not self['overwrite']:
if robj.json:
......@@ -1174,18 +1172,18 @@ class file_download(_pithos_container):
elif path.exists(lpath):
raise CLIError(
'Cannot overwrite %s' % lpath,
details=['To overwrite/resume, use %s' % '/'.join(
self.arguments['resume'].parsed_name)])
details=['To overwrite/resume, use %s' % (
self.arguments['resume'].lvalue)])
else:
ret.append((opath, lpath, None))
elif self.path:
raise CLIError(
'Remote object /%s/%s is a directory' % (
self.container, local_path),
details=['Use %s to download directories' % '/'.join(
self.arguments['recursive'].parsed_name)])
details=['Use %s to download directories' % (
self.arguments['recursive'].lvalue)])
else:
parsed_name = '/'.join(self.arguments['recursive'].parsed_name)
parsed_name = self.arguments['recursive'].lvalue
raise CLIError(
'Cannot download container %s' % self.container,
details=[
......@@ -1197,8 +1195,8 @@ class file_download(_pithos_container):
if path.exists(local_path) and not self['resume']:
raise CLIError(
'Cannot overwrite local file %s' % (lpath),
details=['To overwrite/resume, use %s' % '/'.join(
self.arguments['resume'].parsed_name)])
details=['To overwrite/resume, use %s' % (
self.arguments['resume'].lvalue)])
ret.append((rpath, local_path, self['resume']))
for r, l, resume in ret:
if r:
......@@ -1528,8 +1526,8 @@ class container_delete(_pithos_account):
delimiter, msg = '/', 'Empty and d%s' % msg[1:]
elif num_of_contents:
raise CLIError('Container %s is not empty' % container, details=[
'Use %s to delete non-empty containers' % '/'.join(
self.arguments['recursive'].parsed_name)])
'Use %s to delete non-empty containers' % (
self.arguments['recursive'].lvalue)])
if self['yes'] or self.ask_user(msg):
if num_of_contents:
self.client.del_container(delimiter=delimiter)
......@@ -1658,8 +1656,8 @@ class group_create(_pithos_group, _optional_json):
else:
raise CLISyntaxError(
'No valid users specified, use %s or %s' % (
'/'.join(self.arguments['user_uuid'].parsed_name),
'/'.join(self.arguments['username'].parsed_name)),
self.arguments['user_uuid'].lvalue,
self.arguments['username'].lvalue),
details=[
'Check if a username or uuid is valid with',
' user uuid2username', 'OR', ' user username2uuid'])
......
......@@ -492,3 +492,61 @@ class Client(Logged):
def move(self, path, **kwargs):
return self.request('move', path, **kwargs)
class Waiter(object):
def _wait(
self, item_id, current_status, get_status,
delay=1, max_wait=100, wait_cb=None):
"""Wait for item while its status is current_status
:param server_id: integer (str or int)
:param current_status: (str)
:param get_status: (method(self, item_id)) if called, returns
(status, progress %) If no way to tell progress, return None
:param delay: time interval between retries
:param wait_cb: (method(total steps)) returns a generator for
reporting progress or timeouts i.e., for a progress bar
:returns: (str) the new mode if successful, (bool) False if timed out
"""
status, progress = get_status(self, item_id)
if wait_cb:
wait_gen = wait_cb(max_wait // delay)
wait_gen.next()
if status != current_status:
if wait_cb:
try:
wait_gen.next()
except Exception:
pass
return status
old_wait = total_wait = 0
while status == current_status and total_wait <= max_wait:
if wait_cb:
try:
for i in range(total_wait - old_wait):
wait_gen.next()
except Exception:
break
old_wait = total_wait
total_wait = progress or total_wait + 1
sleep(delay)
status, progress = get_status(self, item_id)
if total_wait < max_wait:
if wait_cb:
try:
for i in range(max_wait):
wait_gen.next()
except:
pass
return status if status != current_status else False
......@@ -128,6 +128,14 @@ class ComputeClient(ComputeRestClient):
:param personality: a list of (file path, file contents) tuples,
describing files to be injected into virtual server upon creation
:param networks: (list of dicts) Networks to connect to, list this:
"networks": [
{"network": <network_uuid>},
{"network": <network_uuid>, "fixed_ip": address},
{"port": <port_id>}, ...]
ATTENTION: Empty list is different to None. None means ' do not
mention it', empty list means 'automatically get an ip'
:returns: a dict with the new virtual server details
:raises ClientError: wraps request errors
......@@ -141,8 +149,8 @@ class ComputeClient(ComputeRestClient):
if personality:
req['server']['personality'] = personality
if networks:
req['server']['networks'] = networks
if networks is not None:
req['server']['networks'] = networks or []
r = self.servers_post(
json_data=req,
......
......@@ -31,15 +31,13 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from time import sleep
from kamaki.clients.cyclades.rest_api import CycladesRestClient
from kamaki.clients.network import NetworkClient
from kamaki.clients.utils import path4url
from kamaki.clients import ClientError
from kamaki.clients import ClientError, Waiter
class CycladesClient(CycladesRestClient):
class CycladesClient(CycladesRestClient, Waiter):
"""Synnefo Cyclades Compute API client"""
def create_server(
......@@ -63,6 +61,8 @@ class CycladesClient(CycladesRestClient):
{"network": <network_uuid>},
{"network": <network_uuid>, "fixed_ip": address},
{"port": <port_id>}, ...]
ATTENTION: Empty list is different to None. None means ' do not
mention it', empty list means 'automatically get an ip'
:returns: a dict with the new virtual server details
......@@ -78,7 +78,7 @@ class CycladesClient(CycladesRestClient):
return super(CycladesClient, self).create_server(
name, flavor_id, image_id,
metadata=metadata, personality=personality)
metadata=metadata, personality=personality, networks=networks)
def start_server(self, server_id):
"""Submit a startup request
......@@ -159,184 +159,6 @@ class CycladesClient(CycladesRestClient):
r = self.servers_stats_get(server_id)
return r.json['stats']
def list_networks(self, detail=False):
"""
:param detail: (bool)
:returns: (list) id,name if not detail else full info per network
"""
detail = 'detail' if detail else ''
r = self.networks_get(command=detail)
return r.json['networks']
def list_network_nics(self, network_id):
"""
:param network_id: integer (str or int)
:returns: (list)
"""
r = self.networks_get(network_id=network_id)
return r.json['network']['attachments']
def create_network(
self, name,
cidr=None, gateway=None, type=None, dhcp=False):
"""
:param name: (str)
:param cidr: (str)
:param geteway: (str)
:param type: (str) if None, will use MAC_FILTERED as default
Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
:param dhcp: (bool)
:returns: (dict) network detailed info
"""
net = dict(name=name)
if cidr:
net['cidr'] = cidr
if gateway:
net['gateway'] = gateway
net['type'] = type or 'MAC_FILTERED'
net['dhcp'] = True if dhcp else False
req = dict(network=net)
r = self.networks_post(json_data=req, success=202)
return r.json['network']
def get_network_details(self, network_id):
"""
:param network_id: integer (str or int)
:returns: (dict)
"""
r = self.networks_get(network_id=network_id)
return r.json['network']
def update_network_name(self, network_id, new_name):
"""
:param network_id: integer (str or int)
:param new_name: (str)
:returns: (dict) response headers
"""
req = {'network': {'name': new_name}}