def _write_attrs(method, name, **kwargs): """ Helper method for reduced code between POST and PUT. Returns a response for the methods calling it. """ if method == 'set': code = 200 if method == 'add': code = 201 else: util.dumps('"%s" is neither set nor add. How did you get here?' % method, 400) request_kwargs = dict(request.params.items()) driver = kwargs.get('driver', None) obj, status, msg = util.get(name, driver) if not obj: return util.dumps(msg, status) try: # Merge URL values and kwarg values, but do not allow conflicts. for k, v in request_kwargs.items(): if kwargs.get(k) is not None and kwargs[k] != v: raise ValueError('Two different values were submitted for "%s": %s' % (k, [kwargs[k], v])) kwargs[k] = v # Additionally capture a value error if the json is bad. json_kwargs = request.json except ValueError as ve: return util.dumps('%s' % (ve,), 400) if json_kwargs: if request.query: return util.dumps('Error: json and query params may not be passed in the same request.', 400) kwargs = json_kwargs # Adds support for bulk attr posting. attrs = [kwargs] if isinstance(kwargs, dict) else kwargs # Check for malformed data or missing pieces before adding any attrs. for attr in attrs: for k in ('key', 'value'): if k not in attr.keys(): bottle.abort(412, 'Provide at least "key" and "value"') if 'number' in attr: try: attr['number'] = int(attr['number']) except ValueError as ve: return util.dumps('%s' % (ve,), 400) if 'datatype' in attr: datatype = attr.pop('datatype') mask = attr.pop('mask', '%Y-%m-%dT%H:%M:%S.%f') attr['value'] = util.typecast(attr['value'], datatype, mask=mask) for attr in attrs: getattr(obj, method + '_attr')(**attr) return util.dumps([util.unclusto(_) for _ in obj.attrs()], code)
def list(driver=None): """ Lists all resource managers found in the clusto database. Optionally you can list all resource managers that match the given ``driver`` Examples: .. code:: bash $ ${post} -d 'name=zmanager' ${server_url}/resourcemanager/simplenamemanager { "attrs": [ ... ], "contents": [], "count": 0, "driver": "simplenamemanager", "name": "zmanager", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json The above will create a simple name manager called "zmanager" .. code:: bash $ ${get} ${server_url}/resourcemanager/ [ "/simpleentitynamemanager/testnames", "/simplenamemanager/zmanager" ] HTTP: 200 Content-type: application/json The above will list all resource managers in clusto, which should have "zmanager" .. code:: bash $ ${get} ${server_url}/resourcemanager/simpleentitynamemanager [ "/simpleentitynamemanager/testnames" ] HTTP: 200 Content-type: application/json Will list all resource managers of driver ``SimpleNameManager`` .. code:: bash $ ${get} ${server_url}/resourcemanager/notadriver "Not a valid driver \"notadriver\" (driver name notadriver doesn't exist.)" HTTP: 404 Content-type: application/json Will return a ``404`` error because that resource manager driver doesn't exist """ result = [] if driver: try: ents = clusto.get_entities(clusto_drivers=[driver]) except NameError as ne: return util.dumps( 'Not a valid driver "%s" (%s)' % (driver, ne,), 404 ) else: # Until we fix the ipmanager snafu, gotta check for both types ents = clusto.get_entities(clusto_types=['resourcemanager']) for ent in ents: # Kind of shitty way, but have to make sure these are all resource managers if issubclass(ent.__class__, drivers.resourcemanagers.ResourceManager): result.append(util.unclusto(ent)) return util.dumps(result)
def deallocate(driver, manager): """ Resource managers should allow you to deallocate *things* just the same as allocating *things*. Examples: .. code:: bash $ ${post} -d 'name=ipman2' -d 'gateway=192.168.1.1' -d 'netmask=255.255.255.0' -d 'baseip=192.168.1.10' ${server_url}/resourcemanager/ipmanager { "attrs": [ { "datatype": "string", "key": "baseip", "number": null, "subkey": "property", "value": "192.168.1.10" }, { "datatype": "string", "key": "gateway", "number": null, "subkey": "property", "value": "192.168.1.1" }, { "datatype": "string", "key": "netmask", "number": null, "subkey": "property", "value": "255.255.255.0" } ], "contents": [], "count": 0, "driver": "ipmanager", "name": "ipman2", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json $ ${post} -d 'name=names2' -d 'basename=a' ${server_url}/resourcemanager/simpleentitynamemanager { "attrs": [ ... ], "contents": [], "count": 0, "driver": "simpleentitynamemanager", "name": "names2", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json $ ${post} -d 'driver=basicserver' ${server_url}/resourcemanager/simpleentitynamemanager/names2 "/basicserver/a01" HTTP: 201 Content-type: application/json $ ${post} -d 'object=a01' ${server_url}/resourcemanager/ipmanager/ipman2 { "datatype": "int", "key": "ip", "number": 0, "subkey": null, "value": 1084752130 } HTTP: 201 Content-type: application/json $ ${delete} -d 'object=a01' ${server_url}/resourcemanager/ipmanager/ipman2 HTTP: 204 Content-type: """ resman, status, msg = _get_resource_manager(manager, driver) if not resman: return util.dumps(msg, status) else: obj = request.params.get('object') if not obj: return util.dumps( 'Cannot deallocate empty, send an object to deallocate', 404 ) obj, status, msg = util.get(obj) resource = request.params.get('resource', ()) # Attempt to deallocate resman.deallocate(obj, resource=resource) return util.dumps(util.unclusto(resman), 204)
def allocate(driver, manager): """ This allocates a new *resource* to a given *thing*. Said thing can be either a *driver* (and the result will be a newly created object subclasses from this driver) or an *object*, and the resource manager will allocate (bind) a resource to it. Examples: .. code:: bash $ ${post} -d 'name=allocator' ${server_url}/resourcemanager/simpleentitynamemanager { "attrs": [ ... ], "contents": [], "count": 0, "driver": "simpleentitynamemanager", "name": "allocator", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json $ ${post} -d 'driver=basicserver' ${server_url}/resourcemanager/simpleentitynamemanager/allocator "/basicserver/01" HTTP: 201 Content-type: application/json Will request a new name from the object ``allocator`` (which is an object of ``SimpleEntityManager`` that we just created with default values) and then it will create a new ``BasicServer`` object. .. code:: bash $ ${post} -d 'driver=basicserver' -d 'resource=99' ${server_url}/resourcemanager/simpleentitynamemanager/allocator "/basicserver/99" HTTP: 201 Content-type: application/json Will create a new ``BasicServer`` object from the ``testnames`` resource manager with the specific name of ``s99``. """ obj, status, msg = _get_resource_manager(manager, driver) if not obj: return util.dumps(msg, status) else: d = request.params.get('driver') o = request.params.get('object') thing = d or o if not thing: return util.dumps( 'Cannot allocate an empty thing, send one of ' '"driver", "object"', 404 ) if d: thing = clusto.driverlist.get(thing) else: thing = clusto.get_by_name(thing) if not thing: return util.dumps('Thing was "%s" not found' % (d or o,), 404) resource = request.params.get('resource', default=()) r = obj.allocate(thing, resource) # The returned value can be anything such a string, number, or attribute return util.dumps(util.unclusto(r), 201)
def create(driver): """ This differs from the standard way of creating entities is that resource managers can have a number of extra parameters added to them that not necessarily match any of the other entities. These parameters are defined by each resource manager driver and are pretty much arbitrary. Seems like a good idea to separate these crucial differences. Examples: .. code:: bash $ ${post} -d 'name=nameman1' ${server_url}/resourcemanager/simplenamemanager { "attrs": [ ... ], "contents": [], "count": 0, "driver": "simplenamemanager", "name": "nameman1", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json Will create a ``SimpleNameManager`` resource manager named ``namemgr1`` with all default values set. .. code:: bash $ ${post} -d 'name=ipman1' -d 'gateway=192.168.1.1' -d 'netmask=255.255.255.0' -d 'baseip=192.168.1.10' ${server_url}/resourcemanager/ipmanager { "attrs": [ { "datatype": "string", "key": "baseip", "number": null, "subkey": "property", "value": "192.168.1.10" }, { "datatype": "string", "key": "gateway", "number": null, "subkey": "property", "value": "192.168.1.1" }, { "datatype": "string", "key": "netmask", "number": null, "subkey": "property", "value": "255.255.255.0" } ], "contents": [], "count": 0, "driver": "ipmanager", "name": "ipman1", "parents": [], "type": "resourcemanager" } HTTP: 201 Content-type: application/json Will create a ``IPManager`` resource manager named ``ipman1`` with some additional arguments such as ``netmask``, ``gateway`` and ``baseip`` """ if driver not in clusto.driverlist: return util.dumps('Requested driver "%s" does not exist' % (driver,), 412) cls = clusto.driverlist[driver] name = request.params.get('name') request.params.pop('name') # Pass any additional parameters as is to the constructor kwargs = {} for param, value in request.params.items(): kwargs[param] = value found = None try: found = util.unclusto(clusto.get_by_name(name)) except LookupError: pass obj = clusto.get_or_create(name, cls, **kwargs) headers = {} if found: headers['Warnings'] = 'Resource manager "%s" already exists' % (found,) code = 201 if found: code = 202 return util.dumps(util.show(obj), code, headers=headers)
def create(driver): """ Creates a new object of the given driver. * Requires HTTP parameters ``name`` Example: .. code:: bash $ ${post} -d 'name=createpool1' ${server_url}/entity/pool [ "/pool/createpool1" ] HTTP: 201 Content-type: application/json Will create a new ``pool1`` object with a ``pool`` driver. If the ``pool1`` object already exists, the status code returned will be 202, and you will see whatever warnings in the ``Warnings`` header: .. code:: bash $ ${post_i} -d 'name=createpool1' ${server_url}/entity/pool HTTP/1.0 202 Accepted ... Warnings: Entity(s) /pool/createpool1 already exist(s)... [ "/pool/createpool1" ] If you try to create a server of an unknown driver, you should receive a 412 status code back: .. code:: bash $ ${post} -d 'name=createobject' ${server_url}/entity/nondriver "Requested driver \"nondriver\" does not exist" HTTP: 412 Content-type: application/json The following example: .. code:: bash $ ${post_i} -d 'name=createpool1' -d 'name=createpool2' ${server_url}/entity/pool HTTP/1.0 202 Accepted ... Warnings: Entity(s) /pool/createpool1 already exist(s)... [ "/pool/createpool1", "/pool/createpool2" ] Will attempt to create new objects ``createpool1`` and ``createpool2`` with a ``pool`` driver. As all objects are validated prior to creation, if any of them already exists the return code will be 202 (Accepted) and you will get an extra header ``Warnings`` with the message. """ if driver not in clusto.driverlist: return util.dumps('Requested driver "%s" does not exist' % (driver,), 412) cls = clusto.driverlist[driver] names = request.params.getall('name') request.params.pop('name') found = [] for name in names: try: found.append(util.unclusto(clusto.get_by_name(name))) except LookupError: pass result = [] for name in names: result.append(util.unclusto(clusto.get_or_create(name, cls))) headers = {} if found: headers['Warnings'] = 'Entity(s) %s already exist(s)' % (','.join(found),) code = 201 if found: code = 202 return util.dumps(result, code, headers=headers)
def list(driver=None): """ Returns all entities, or (optionally) all entities of the given driver Example: .. code:: bash $ ${get} ${server_url}/entity/ [ ... ] HTTP: 200 Content-type: application/json Will list all entities Example: .. code:: bash $ ${get} ${server_url}/entity/clustometa [ "/clustometa/clustometa" ] HTTP: 200 Content-type: application/json Will list all entities that match the driver ``clustometa`` The following example should fail because there is no driver ``nondriver``: .. code:: bash $ ${get} ${server_url}/entity/nondriver "The requested driver \"nondriver\" does not exist" HTTP: 412 Content-type: application/json """ result = [] kwargs = {} for param in request.params.keys(): kwargs[param] = request.params.getall(param) if driver: if driver in clusto.driverlist: kwargs['clusto_drivers'] = [clusto.driverlist[driver]] else: return util.dumps('The requested driver "%s" does not exist' % (driver,), 412) ents = clusto.get_entities(**kwargs) for ent in ents: result.append(util.unclusto(ent)) result = [] kwargs = {} for param in request.params.keys(): kwargs[param] = request.params.getall(param) if driver: if driver in clusto.driverlist: kwargs['clusto_drivers'] = [clusto.driverlist[driver]] ents = clusto.get_entities(**kwargs) for ent in ents: result.append(util.unclusto(ent)) return util.dumps(result)
def attrs(name, key=None, subkey=None, number=None): """ Query attributes from this object. Example: .. code:: bash $ ${post} -d 'name=attrpool1' ${server_url}/entity/pool [ "/pool/attrpool1" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${get} ${server_url}/attribute/attrpool1 [] HTTP: 200 Content-type: application/json Will show all the attributes from the object ``attrpool1``: .. code:: bash $ ${get} -d 'driver=pool' ${server_url}/attribute/attrpool1 [] HTTP: 200 Content-type: application/json Will show all the attributes from the object ``attrpool1`` **if** the driver for ``attrpool1`` is ``pool``. In the same vein this code: .. code:: bash $ ${get} -d 'driver=basicserver' ${server_url}/attribute/attrpool1 ... HTTP: 409 ... Should fail, because the ``attrpool1`` object is of type ``pool``, **not** ``basicserver`` Example: .. code:: bash $ ${get} ${server_url}/attribute/attrpool1/owner [] HTTP: 200 Content-type: application/json Will show the attributes for ``server1`` if their key is ``owner``. """ attrs = [] kwargs = dict(request.params.items()) driver = kwargs.get('driver', None) obj, status, msg = util.get(name, driver) if not obj: return util.dumps(msg, status) qkwargs = {} if key: qkwargs['key'] = key if subkey: qkwargs['subkey'] = subkey if number: qkwargs['number'] = number for attr in obj.attrs(**qkwargs): attrs.append(util.unclusto(attr)) return util.dumps(attrs)
def del_attrs(name, key, subkey=None, number=None): """ Deletes an attribute from this object * Requires HTTP path ``key`` * Optional parameters are ``subkey``, ``value``, and ``number`` Examples: .. code:: bash $ ${post} -d 'name=deleteserver1' ${server_url}/entity/basicserver [ "/basicserver/deleteserver1" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${put} -d 'value=joe' ${server_url}/attribute/deleteserver1/group/owner [ { "datatype": "string", "key": "group", "number": null, "subkey": "owner", "value": "joe" } ] HTTP: 200 Content-type: application/json .. code:: bash $ ${delete} ${server_url}/attribute/deleteserver1/group/owner [] HTTP: 200 Content-type: application/json Will create a ``basicserver`` object called ``deleteserver1``, then it will add an attribute (the only attribute so far), then it will delete it. .. code:: bash $ ${post} -d 'name=deleteserver2' ${server_url}/entity/basicserver [ "/basicserver/deleteserver2" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${put} -d 'value=engineering' ${server_url}/attribute/deleteserver2/group [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "engineering" } ] HTTP: 200 Content-type: application/json .. code:: bash $ ${put} -d 'value=joe' ${server_url}/attribute/deleteserver2/group/owner [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "engineering" }, { "datatype": "string", "key": "group", "number": null, "subkey": "owner", "value": "joe" } ] HTTP: 200 Content-type: application/json .. code:: bash $ ${delete} ${server_url}/attribute/deleteserver2/group/owner [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "engineering" } ] HTTP: 200 Content-type: application/json This example should add two attributes with the same key, but different subkey, then it will delete only the second value. """ kwargs = dict(request.params.items()) driver = kwargs.get('driver', None) obj, status, msg = util.get(name, driver) if not obj: return util.dumps(msg, status) qkwargs = {'key': key} if subkey: qkwargs['subkey'] = subkey if number: qkwargs['number'] = number obj.del_attrs(**qkwargs) return util.dumps([util.unclusto(_) for _ in obj.attrs()])
def create(driver): """ Creates a new object of the given driver. * Requires HTTP parameters ``name`` Example: .. code:: bash $ ${post} -d 'name=createpool1' ${server_url}/entity/pool [ "/pool/createpool1" ] HTTP: 201 Content-type: application/json Will create a new ``pool1`` object with a ``pool`` driver. If the ``pool1`` object already exists, the status code returned will be 202, and you will see whatever warnings in the ``Warnings`` header: .. code:: bash $ ${post_i} -d 'name=createpool1' ${server_url}/entity/pool HTTP/1.0 202 Accepted ... Warnings: Entity(s) /pool/createpool1 already exist(s)... [ "/pool/createpool1" ] If you try to create a server of an unknown driver, you should receive a 412 status code back: .. code:: bash $ ${post} -d 'name=createobject' ${server_url}/entity/nondriver "Requested driver \"nondriver\" does not exist" HTTP: 412 Content-type: application/json The following example: .. code:: bash $ ${post_i} -d 'name=createpool1' -d 'name=createpool2' ${server_url}/entity/pool HTTP/1.0 202 Accepted ... Warnings: Entity(s) /pool/createpool1 already exist(s)... [ "/pool/createpool1", "/pool/createpool2" ] Will attempt to create new objects ``createpool1`` and ``createpool2`` with a ``pool`` driver. As all objects are validated prior to creation, if any of them already exists the return code will be 202 (Accepted) and you will get an extra header ``Warnings`` with the message. """ if driver not in clusto.driverlist: return util.dumps('Requested driver "%s" does not exist' % (driver, ), 412) cls = clusto.driverlist[driver] names = request.params.getall('name') request.params.pop('name') found = [] for name in names: try: found.append(util.unclusto(clusto.get_by_name(name))) except LookupError: pass result = [] for name in names: result.append(util.unclusto(clusto.get_or_create(name, cls))) headers = {} if found: headers['Warnings'] = 'Entity(s) %s already exist(s)' % ( ','.join(found), ) code = 201 if found: code = 202 return util.dumps(result, code, headers=headers)
def _write_attrs(method, name, **kwargs): """ Helper method for reduced code between POST and PUT. Returns a response for the methods calling it. """ if method == 'set': code = 200 if method == 'add': code = 201 else: util.dumps( '"%s" is neither set nor add. How did you get here?' % method, 400) request_kwargs = dict(request.params.items()) driver = kwargs.get('driver', None) obj, status, msg = util.get(name, driver) if not obj: return util.dumps(msg, status) try: # Merge URL values and kwarg values, but do not allow conflicts. for k, v in request_kwargs.items(): if kwargs.get(k) is not None and kwargs[k] != v: raise ValueError( 'Two different values were submitted for "%s": %s' % (k, [kwargs[k], v])) kwargs[k] = v # Additionally capture a value error if the json is bad. json_kwargs = request.json except ValueError as ve: return util.dumps('%s' % (ve, ), 400) if json_kwargs: if request.query: return util.dumps( 'Error: json and query params may not be passed in the same request.', 400) kwargs = json_kwargs # Adds support for bulk attr posting. attrs = [kwargs] if isinstance(kwargs, dict) else kwargs # Check for malformed data or missing pieces before adding any attrs. for attr in attrs: for k in ('key', 'value'): if k not in attr.keys(): bottle.abort(412, 'Provide at least "key" and "value"') if 'number' in attr: try: attr['number'] = int(attr['number']) except ValueError as ve: return util.dumps('%s' % (ve, ), 400) if 'datatype' in attr: datatype = attr.pop('datatype') mask = attr.pop('mask', '%Y-%m-%dT%H:%M:%S.%f') attr['value'] = util.typecast(attr['value'], datatype, mask=mask) for attr in attrs: getattr(obj, method + '_attr')(**attr) return util.dumps([util.unclusto(_) for _ in obj.attrs()], code)
def set_attr(name, key, subkey=None, number=None): """ Sets an attribute from this object. If the attribute doesn't exist it will be added, if the attribute already exists then it will be updated. * Requires HTTP parameters ``key`` and ``value`` * Optional parameters are ``subkey`` and ``number`` Example: .. code:: bash $ ${post} -d 'name=setattrserver' ${server_url}/entity/basicserver [ "/basicserver/setattrserver" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${post} -d 'key=group' -d 'value=web' ${server_url}/attribute/setattrserver [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "web" } ] HTTP: 201 Content-type: application/json .. code:: bash $ ${put} -d 'value=db' ${server_url}/attribute/setattrserver/group [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "db" } ] HTTP: 200 Content-type: application/json Will: #. Create the entity ``setattrserver`` #. Add the attribute with ``key=group`` and ``value=web`` #. Update the attribute to ``value=db`` Example: .. code:: bash $ ${post} -d 'name=setattrserver2' ${server_url}/entity/basicserver [ "/basicserver/setattrserver2" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${put} -d 'value=joe' ${server_url}/attribute/setattrserver2/group/owner [ { "datatype": "string", "key": "group", "number": null, "subkey": "owner", "value": "joe" } ] HTTP: 200 Content-type: application/json .. code:: bash $ ${put} -d 'value=bob' ${server_url}/attribute/setattrserver2/group/owner [ { "datatype": "string", "key": "group", "number": null, "subkey": "owner", "value": "bob" } ] HTTP: 200 Content-type: application/json Will: #. Create a new object ``setattrserver2`` of type ``basicserver`` #. Set the attribute with key ``group`` *and* subkey ``owner`` with value ``joe`` to the object ``setattrserver1``. Since this is the only attribute so far, this operation works just like ``add_attr()`` #. Update the attribute we set above, now the ``value`` will read ``bob`` """ kwargs = dict(request.params.items()) obj, status, msg = util.get(name) if not obj: return util.dumps(msg, status) if 'value' not in kwargs.keys(): bottle.abort(412, 'Provide at least "key" and "value"') obj.set_attr(key=key, subkey=subkey, number=number, value=kwargs['value']) return util.dumps([util.unclusto(_) for _ in obj.attrs()])
def add_attr(name): """ Add an attribute to this object. * Requires HTTP parameters ``name``, ``key``, and ``value`` * Optional parameters are ``subkey`` and ``number`` Example: .. code:: bash $ ${post} -d 'name=addattrserver' ${server_url}/entity/basicserver [ "/basicserver/addattrserver" ] HTTP: 201 Content-type: application/json .. code:: bash $ ${post} -d 'key=group' -d 'value=web' ${server_url}/attribute/addattrserver [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "web" } ] HTTP: 201 Content-type: application/json Will: #. Create an entity called ``addattrserver`` #. Add the attribute with ``key=group`` and ``value=web`` to it Example: .. code:: bash $ ${post} -d 'key=group' -d 'subkey=owner' -d 'value=web' ${server_url}/attribute/addattrserver [ { "datatype": "string", "key": "group", "number": null, "subkey": null, "value": "web" }, { "datatype": "string", "key": "group", "number": null, "subkey": "owner", "value": "web" } ] HTTP: 201 Content-type: application/json Will add the attribute with key ``group`` *and* subkey ``owner`` *and* value ``joe`` to the previously created entity ``addattrserver`` """ kwargs = dict(request.params.items()) driver = kwargs.get('driver', None) obj, status, msg = util.get(name, driver) if not obj: return util.dumps(msg, status) for k in ('key', 'value'): if k not in kwargs.keys(): bottle.abort(412, 'Provide at least "key" and "value"') if 'number' in kwargs: kwargs['number'] = int(kwargs['number']) obj.add_attr(**kwargs) return util.dumps([util.unclusto(_) for _ in obj.attrs()], 201)