Commit eb647cfe authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Allow ports without device_id in lib + waits

Refs: #4624, #4563
parent c6afee48
......@@ -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)])
......
......@@ -415,13 +415,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 +479,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'],
......
......@@ -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'])
......
......@@ -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,
......
......@@ -61,6 +61,8 @@ class CycladesClient(CycladesRestClient, Waiter):
{"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
......@@ -76,7 +78,7 @@ class CycladesClient(CycladesRestClient, Waiter):
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
......@@ -157,130 +159,6 @@ class CycladesClient(CycladesRestClient, Waiter):
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}}
r = self.networks_put(network_id=network_id, json_data=req)
return r.headers
def delete_network(self, network_id):
"""
:param network_id: integer (str or int)
:returns: (dict) response headers
:raises ClientError: 421 Network in use
"""
try:
r = self.networks_delete(network_id)
return r.headers
except ClientError as err:
if err.status == 421:
err.details = [
'Network may be still connected to at least one server']
raise
def connect_server(self, server_id, network_id):
""" Connect a server to a network
:param server_id: integer (str or int)
:param network_id: integer (str or int)
:returns: (dict) response headers
"""
req = {'add': {'serverRef': server_id}}
r = self.networks_post(network_id, 'action', json_data=req)
return r.headers
def disconnect_server(self, server_id, nic_id):
"""
:param server_id: integer (str or int)
:param nic_id: (str)
:returns: (int) the number of nics disconnected
"""
vm_nets = self.list_server_nics(server_id)
num_of_disconnections = 0
for (nic_id, network_id) in [(
net['id'],
net['network_id']) for net in vm_nets if nic_id == net['id']]:
req = {'remove': {'attachment': '%s' % nic_id}}
self.networks_post(network_id, 'action', json_data=req)
num_of_disconnections += 1
return num_of_disconnections
def disconnect_network_nics(self, netid):
"""
:param netid: integer (str or int)
"""
for nic in self.list_network_nics(netid):
req = dict(remove=dict(attachment=nic))
self.networks_post(netid, 'action', json_data=req)
def wait_server(
self, server_id,
current_status='BUILD',
......@@ -308,31 +186,6 @@ class CycladesClient(CycladesRestClient, Waiter):
return self._wait(
server_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_network(
self, net_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
"""Wait for network while its status is current_status
:param net_id: integer (str or int)
:param current_status: (str) PENDING | ACTIVE | DELETED
:param delay: time interval between retries
:max_wait: (int) timeout in secconds
:param wait_cb: if set a progressbar is used to show progress
:returns: (str) the new mode if succesfull, (bool) False if timed out
"""
def get_status(self, net_id):
r = self.get_network_details(net_id)
return r['status'], None
return self._wait(
net_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_firewall(
self, server_id,
current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
......@@ -358,7 +211,7 @@ class CycladesClient(CycladesRestClient, Waiter):
server_id, current_status, get_status, delay, max_wait, wait_cb)
class CycladesNetworkClient(NetworkClient, Waiter):
class CycladesNetworkClient(NetworkClient):
"""Cyclades Network API extentions"""
network_types = (
......@@ -379,14 +232,16 @@ class CycladesNetworkClient(NetworkClient, Waiter):
return r.json['network']
def create_port(
self, network_id, device_id,
security_groups=None, name=None, fixed_ips=None):
port = dict(network_id=network_id, device_id=device_id)
self, network_id,
device_id=None, security_groups=None, name=None, fixed_ips=None):
port = dict(network_id=network_id)
if device_id:
port['device_id'] = device_id
if security_groups:
port['security_groups'] = security_groups
if name:
port['name'] = name
for fixed_ip in fixed_ips:
for fixed_ip in fixed_ips or []:
diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
if diff:
raise ValueError(
......@@ -396,24 +251,11 @@ class CycladesNetworkClient(NetworkClient, Waiter):
r = self.ports_post(json_data=dict(port=port), success=201)
return r.json['port']
def wait_network(
self, net_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def create_floatingip(self, floating_network_id, floating_ip_address=''):
return super(CycladesNetworkClient, self).create_floatingip(
floating_network_id, floating_ip_address=floating_ip_address)
def get_status(self, net_id):
r = self.get_network_details(net_id)
return r['status'], None
return self._wait(
net_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_port(
self, port_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def get_status(self, net_id):
r = self.get_port_details(port_id)
return r['status'], None
return self._wait(
port_id, current_status, get_status, delay, max_wait, wait_cb)
def update_floatingip(self, floating_network_id, floating_ip_address=''):
"""To nullify something optional, use None"""
return super(CycladesNetworkClient, self).update_floatingip(
floating_network_id, floating_ip_address=floating_ip_address)
......@@ -31,11 +31,11 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from kamaki.clients import ClientError
from kamaki.clients import ClientError, Waiter
from kamaki.clients.network.rest_api import NetworkRestClient
class NetworkClient(NetworkRestClient):
class NetworkClient(NetworkRestClient, Waiter):
"""OpenStack Network API 2.0 client"""
def list_networks(self):
......@@ -362,3 +362,50 @@ class NetworkClient(NetworkRestClient):
def delete_floatingip(self, floatingip_id):
r = self.floatingips_delete(floatingip_id, success=204)
return r.headers
# Wait methods
def wait_network(
self, net_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def get_status(self, net_id):
r = self.get_network_details(net_id)
return r['status'], None
return self._wait(
net_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_subnet(
self, subnet_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def get_status(self, subnet_id):
r = self.get_subnet_details(subnet_id)
return r['status'], None
return self._wait(
subnet_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_port(
self, port_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def get_status(self, net_id):
r = self.get_port_details(port_id)
return r['status'], None
return self._wait(
port_id, current_status, get_status, delay, max_wait, wait_cb)
def wait_floatingip(
self, floatingip_id,
current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
def get_status(self, floatingip_id):
r = self.get_network_details(floatingip_id)
return r['status'], None
return self._wait(
floatingip_id,
current_status, get_status, delay, max_wait, wait_cb)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment