Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:12.2
xen
snapshot-xend.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File snapshot-xend.patch of Package xen
Index: xen-4.1.2-testing/tools/python/xen/xend/image.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/image.py +++ xen-4.1.2-testing/tools/python/xen/xend/image.py @@ -490,7 +490,7 @@ class ImageHandler: domains.domains_lock.acquire() - def signalDeviceModel(self, cmd, ret, par = None): + def signalDeviceModel(self, cmd, ret, par = None, timeout = True): if self.device_model is None: return # Signal the device model to for action @@ -527,10 +527,17 @@ class ImageHandler: while state != ret: state = xstransact.Read("/local/domain/0/device-model/%i/state" % self.vm.getDomid()) + if state == 'error': + msg = ("The device model returned an error: %s" + % xstransact.Read("/local/domain/0/device-model/%i/error" + % self.vm.getDomid())) + raise VmError(msg) + time.sleep(0.1) - count += 1 - if count > 100: - raise VmError('Timed out waiting for device model action') + if timeout: + count += 1 + if count > 100: + raise VmError('Timed out waiting for device model action') #resotre orig state xstransact.Store("/local/domain/0/device-model/%i" @@ -555,6 +562,10 @@ class ImageHandler: except: pass + def snapshotDeviceModel(self, name): + # Signal the device model to perform snapshot operation + self.signalDeviceModel('snapshot', 'paused', name, False) + def recreate(self): if self.device_model is None: return Index: xen-4.1.2-testing/tools/python/xen/xend/server/blkif.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/server/blkif.py +++ xen-4.1.2-testing/tools/python/xen/xend/server/blkif.py @@ -88,6 +88,10 @@ class BlkifController(DevController): if bootable != None: back['bootable'] = str(bootable) + if 'snapshotname' in self.vm.info: + back['snapshot'] = self.vm.info['snapshotname'] + self.vm.info.pop('snapshotname') + if security.on() == xsconstants.XS_POLICY_USE: self.do_access_control(config, uname) Index: xen-4.1.2-testing/tools/python/xen/xend/server/SrvDomain.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/server/SrvDomain.py +++ xen-4.1.2-testing/tools/python/xen/xend/server/SrvDomain.py @@ -95,6 +95,31 @@ class SrvDomain(SrvDir): def do_save(self, _, req): return self.xd.domain_save(self.dom.domid, req.args['file'][0]) + def op_snapshot_create(self, op, req): + self.acceptCommand(req) + return req.threadRequest(self.do_snapshot_create, op, req) + + def do_snapshot_create(self, _, req): + return self.xd.domain_snapshot_create(self.dom.domid, req.args['name'][0]) + + def op_snapshot_list(self, op, req): + self.acceptCommand(req) + return self.xd.domain_snapshot_list(self.dom.getName()) + + def op_snapshot_apply(self, op, req): + self.acceptCommand(req) + return req.threadRequest(self.do_snapshot_apply, op, req) + + def do_snapshot_apply(self, _, req): + return self.xd.domain_snapshot_apply(self.dom.getName(), req.args['name'][0]) + + def op_snapshot_delete(self, op, req): + self.acceptCommand(req) + return req.threadRequest(self.do_snapshot_delete, op, req) + + def do_snapshot_delete(self, _, req): + return self.xd.domain_snapshot_delete(self.dom.getName(), req.args['name'][0]) + def op_dump(self, op, req): self.acceptCommand(req) return req.threadRequest(self.do_dump, op, req) @@ -273,7 +298,7 @@ class SrvDomain(SrvDir): def render_GET(self, req): op = req.args.get('op') - if op and op[0] in ['vcpuinfo']: + if op and op[0] in ['vcpuinfo', 'snapshot_list']: return self.perform(req) # Index: xen-4.1.2-testing/tools/python/xen/xend/XendCheckpoint.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/XendCheckpoint.py +++ xen-4.1.2-testing/tools/python/xen/xend/XendCheckpoint.py @@ -65,7 +65,7 @@ def insert_after(list, pred, value): return -def save(fd, dominfo, network, live, dst, checkpoint=False, node=-1,sock=None): +def save(fd, dominfo, network, live, dst, checkpoint=False, node=-1, sock=None, name=None, diskonly=False): from xen.xend import XendDomain try: @@ -78,6 +78,8 @@ def save(fd, dominfo, network, live, dst write_exact(fd, SIGNATURE, "could not write guest state file: signature") sxprep = dominfo.sxpr() + if name: + sxprep.append(['snapshotname', name]) if node > -1: insert_after(sxprep,'vcpus',['node', str(node)]) @@ -112,52 +114,61 @@ def save(fd, dominfo, network, live, dst image_cfg = dominfo.info.get('image', {}) hvm = dominfo.info.is_hvm() - # xc_save takes three customization parameters: maxit, max_f, and - # flags the last controls whether or not save is 'live', while the - # first two further customize behaviour when 'live' save is - # enabled. Passing "0" simply uses the defaults compiled into - # libxenguest; see the comments and/or code in xc_linux_save() for - # more information. - cmd = [xen.util.auxbin.pathTo(XC_SAVE), str(fd), - str(dominfo.getDomid()), "0", "0", - str(int(live) | (int(hvm) << 2)) ] - log.debug("[xc_save]: %s", string.join(cmd)) - - def saveInputHandler(line, tochild): - log.debug("In saveInputHandler %s", line) - if line == "suspend": - log.debug("Suspending %d ...", dominfo.getDomid()) - dominfo.shutdown('suspend') - dominfo.waitForSuspend() - if line in ('suspend', 'suspended'): - dominfo.migrateDevices(network, dst, DEV_MIGRATE_STEP2, - domain_name) - log.info("Domain %d suspended.", dominfo.getDomid()) - dominfo.migrateDevices(network, dst, DEV_MIGRATE_STEP3, - domain_name) - if hvm: - dominfo.image.saveDeviceModel() - - if line == "suspend": - tochild.write("done\n") - tochild.flush() - log.debug('Written done') - - forkHelper(cmd, fd, saveInputHandler, False) - - # put qemu device model state - if os.path.exists("/var/lib/xen/qemu-save.%d" % dominfo.getDomid()): - write_exact(fd, QEMU_SIGNATURE, "could not write qemu signature") - qemu_fd = os.open("/var/lib/xen/qemu-save.%d" % dominfo.getDomid(), - os.O_RDONLY) - while True: - buf = os.read(qemu_fd, dm_batch) - if len(buf): - write_exact(fd, buf, "could not write device model state") - else: - break - os.close(qemu_fd) - os.remove("/var/lib/xen/qemu-save.%d" % dominfo.getDomid()) + if not diskonly: + # xc_save takes three customization parameters: maxit, max_f, and + # flags the last controls whether or not save is 'live', while the + # first two further customize behaviour when 'live' save is + # enabled. Passing "0" simply uses the defaults compiled into + # libxenguest; see the comments and/or code in xc_linux_save() for + # more information. + cmd = [xen.util.auxbin.pathTo(XC_SAVE), str(fd), + str(dominfo.getDomid()), "0", "0", + str(int(live) | (int(hvm) << 2)) ] + log.debug("[xc_save]: %s", string.join(cmd)) + + def saveInputHandler(line, tochild): + log.debug("In saveInputHandler %s", line) + if line == "suspend": + log.debug("Suspending %d ...", dominfo.getDomid()) + dominfo.shutdown('suspend') + dominfo.waitForSuspend() + if line in ('suspend', 'suspended'): + dominfo.migrateDevices(network, dst, DEV_MIGRATE_STEP2, + domain_name) + log.info("Domain %d suspended.", dominfo.getDomid()) + dominfo.migrateDevices(network, dst, DEV_MIGRATE_STEP3, + domain_name) + if hvm: + dominfo.image.saveDeviceModel() + if name: + dominfo.image.resumeDeviceModel() + + if line == "suspend": + tochild.write("done\n") + tochild.flush() + log.debug('Written done') + + forkHelper(cmd, fd, saveInputHandler, False) + + # put qemu device model state + if os.path.exists("/var/lib/xen/qemu-save.%d" % dominfo.getDomid()): + write_exact(fd, QEMU_SIGNATURE, "could not write qemu signature") + qemu_fd = os.open("/var/lib/xen/qemu-save.%d" % dominfo.getDomid(), + os.O_RDONLY) + while True: + buf = os.read(qemu_fd, dm_batch) + if len(buf): + write_exact(fd, buf, "could not write device model state") + else: + break + os.close(qemu_fd) + os.remove("/var/lib/xen/qemu-save.%d" % dominfo.getDomid()) + else: + dominfo.shutdown('suspend') + dominfo.waitForShutdown() + + if name: + dominfo.image.snapshotDeviceModel(name) if checkpoint: dominfo.resumeDomain() @@ -231,6 +242,71 @@ def restore(xd, fd, dominfo = None, paus if othervm is not None and othervm.domid is not None: raise VmError("Domain '%s' already exists with ID '%d'" % (domconfig["name_label"], othervm.domid)) + def contains_state(fd): + try: + cur = os.lseek(fd, 0, 1) + end = os.lseek(fd, 0, 2) + + ret = False + if cur < end: + ret = True + + os.lseek(fd, cur, 0) + return ret + except OSError, (errno, strerr): + # lseek failed <==> socket <==> state + return True + + # + # We shouldn't hold the domains_lock over a waitForDevices + # As this function sometime gets called holding this lock, + # we must release it and re-acquire it appropriately + # + def wait_devs(dominfo): + from xen.xend import XendDomain + + lock = True; + try: + XendDomain.instance().domains_lock.release() + except: + lock = False; + + try: + dominfo.waitForDevices() # Wait for backends to set up + except Exception, exn: + log.exception(exn) + if lock: + XendDomain.instance().domains_lock.acquire() + raise + + if lock: + XendDomain.instance().domains_lock.acquire() + + + if not contains_state(fd): + # Disk-only snapshot. Just start the vm from config (which should + # contain snapshotname. + if dominfo: + log.debug("### starting domain directly through XendDomainInfo") + dominfo.start() + else: + # Warning! Do we need to call into XendDomain to get domain + # lock? Similar to the xd.restore_() call below? + # We'll try XendDomain.domain_create() + log.debug("### starting domain through XendDomain.create()") + dominfo = xd.domain_create(vmconfig) + + try: + wait_devs(dominfo) + except: + dominfo.destroy() + raise + + dominfo.unpause() + + # Done if disk only snapshot + return dominfo + if dominfo: dominfo.resume() else: @@ -332,24 +408,7 @@ def restore(xd, fd, dominfo = None, paus dominfo.completeRestore(handler.store_mfn, handler.console_mfn) - # - # We shouldn't hold the domains_lock over a waitForDevices - # As this function sometime gets called holding this lock, - # we must release it and re-acquire it appropriately - # - from xen.xend import XendDomain - - lock = True; - try: - XendDomain.instance().domains_lock.release() - except: - lock = False; - - try: - dominfo.waitForDevices() # Wait for backends to set up - finally: - if lock: - XendDomain.instance().domains_lock.acquire() + wait_devs(dominfo) if not paused: dominfo.unpause() Index: xen-4.1.2-testing/tools/python/xen/xend/XendConfig.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/XendConfig.py +++ xen-4.1.2-testing/tools/python/xen/xend/XendConfig.py @@ -244,6 +244,7 @@ XENAPI_CFG_TYPES = { 'memory_sharing': int, 'pool_name' : str, 'Description': str, + 'snapshotname': str, } # List of legacy configuration keys that have no equivalent in the Index: xen-4.1.2-testing/tools/python/xen/xend/XendDomain.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/XendDomain.py +++ xen-4.1.2-testing/tools/python/xen/xend/XendDomain.py @@ -53,6 +53,7 @@ from xen.xend.xenstore.xstransact import from xen.xend.xenstore.xswatch import xswatch from xen.util import mkdir, rwlock from xen.xend import uuid +from xen.xend import sxp xc = xen.lowlevel.xc.xc() xoptions = XendOptions.instance() @@ -319,11 +320,16 @@ class XendDomain: fd, fn = tempfile.mkstemp() f = os.fdopen(fd, 'w+b') try: + snapshotname = '' + if dominfo.info.has_key('snapshotname'): + snapshotname = dominfo.info.pop('snapshotname') + prettyprint(dominfo.sxpr(legacy_only = False), f, width = 78) finally: f.close() - + if snapshotname: + dominfo.info['snapshotname'] = snapshotname try: shutil.move(fn, self._managed_config_path(dom_uuid)) except: @@ -1585,6 +1591,187 @@ class XendDomain: else: log.debug("error: Domain is not running!") + def domain_snapshot_create(self, domid, name, diskonly=False): + """Snapshot a running domain. + + @param domid: Domain ID or Name + @type domid: int or string. + @param name: Snapshot name + @type dst: string + @param diskonly: Snapshot disk only - exclude machine state + @type dst: bool + @rtype: None + @raise XendError: Failed to snapshot domain + @raise XendInvalidDomain: Domain is not valid + """ + try: + dominfo = self.domain_lookup_nr(domid) + if not dominfo: + raise XendInvalidDomain(str(domid)) + + snap_file = os.path.join(xoptions.get_xend_domains_path(), + dominfo.get_uuid(), "snapshots", name) + + if os.access(snap_file, os.F_OK): + raise XendError("Snapshot:%s exist for domain %s\n" % (name, str(domid))) + + if dominfo.getDomid() == DOM0_ID: + raise XendError("Cannot snapshot privileged domain %s" % str(domid)) + if dominfo._stateGet() != DOM_STATE_RUNNING: + raise VMBadState("Domain is not running", + POWER_STATE_NAMES[DOM_STATE_RUNNING], + POWER_STATE_NAMES[dominfo._stateGet()]) + + if not os.path.exists(self._managed_config_path(dominfo.get_uuid())): + raise XendError("Domain is not managed by Xend lifecycle " + + "support.") + + # Check if all images support snapshots + for dev_type, dev_info in dominfo.info.all_devices_sxpr(): + mode = sxp.child_value(dev_info, 'mode') + if mode == 'r': + continue; + if dev_type == 'vbd': + raise XendError("All writable images need to use the " + + "tap:qcow2 protocol for snapshot support") + if dev_type == 'tap': + # Fetch the protocol name from tap:xyz:filename + type = sxp.child_value(dev_info, 'uname') + type = type.split(':')[1] + if type != 'qcow2': + raise XendError("All writable images need to use the " + + "tap:qcow2 protocol for snapshot support") + + snap_path = os.path.join(xoptions.get_xend_domains_path(), + dominfo.get_uuid(), "snapshots") + mkdir.parents(snap_path, stat.S_IRWXU) + snap_file = os.path.join(snap_path, name) + + + oflags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + if hasattr(os, "O_LARGEFILE"): + oflags |= os.O_LARGEFILE + fd = os.open(snap_file, oflags) + try: + XendCheckpoint.save(fd, dominfo, False, False, snap_file, + True, sock=None, name=name, diskonly=diskonly) + except Exception, e: + os.close(fd) + os.unlink(snap_file) + raise e + os.close(fd) + except OSError, ex: + raise XendError("can't write guest state file %s: %s" % + (snap_file, ex[1])) + + def domain_snapshot_list(self, domid): + """List available snapshots for a domain. + + @param domid: Domain ID or Name + @type domid: int or string. + @rtype: list of snapshot names + @raise XendInvalidDomain: Domain is not valid + """ + try: + dominfo = self.domain_lookup_nr(domid) + if not dominfo: + raise XendInvalidDomain(str(domid)) + + snap_path = os.path.join(xoptions.get_xend_domains_path(), + dominfo.get_uuid(), "snapshots") + + if not os.access(snap_path, os.R_OK): + return [] + + return os.listdir(snap_path) + + except: + return [] + + def domain_snapshot_apply(self, domid, name): + """Start a domain from snapshot + + @param domid: Domain ID or Name + @type domid: int or string. + @param name: Snapshot name + @type dst: string + @rtype: None + @raise XendError: Failed to apply snapshot + @raise XendInvalidDomain: Domain is not valid + """ + try: + dominfo = self.domain_lookup_nr(domid) + if not dominfo: + log.debug("## no dominfo") + raise XendInvalidDomain(str(domid)) + + if dominfo.getDomid() == DOM0_ID: + raise XendError("Cannot apply snapshots to privileged domain %s" % str(domid)) + if dominfo._stateGet() != DOM_STATE_HALTED: + raise VMBadState("Domain is not halted", + POWER_STATE_NAMES[DOM_STATE_HALTED], + POWER_STATE_NAMES[dominfo._stateGet()]) + + snap_file = os.path.join(xoptions.get_xend_domains_path(), + dominfo.get_uuid(), "snapshots", name) + if not os.access(snap_file, os.R_OK): + raise XendError("Unable to access snapshot %s for domain %s" % + (name, str(domid))) + + oflags = os.O_RDONLY + if hasattr(os, "O_LARGEFILE"): + oflags |= os.O_LARGEFILE + fd = os.open(snap_file, oflags) + try: + self.domain_restore_fd(fd) + finally: + os.close(fd) + except OSError, ex: + raise XendError("Unable to read snapshot file file %s: %s" % + (snap_file, ex[1])) + + def domain_snapshot_delete(self, domid, name): + """Delete domain snapshot + + @param domid: Domain ID or Name + @type domid: int or string. + @param name: Snapshot name + @type domid: string + @rtype: None + @raise XendInvalidDomain: Domain is not valid + """ + dominfo = self.domain_lookup_nr(domid) + if not dominfo: + raise XendInvalidDomain(str(domid)) + + snap_file = os.path.join(xoptions.get_xend_domains_path(), + dominfo.get_uuid(), "snapshots", name) + + if not os.access(snap_file, os.F_OK): + raise XendError("Snapshot %s does not exist for domain %s" % + (name, str(domid))) + + # Need to "remove" snapshot from qcow2 image file. + # For running domains, this is left to ioemu. For stopped domains + # we must invoke qemu-img for all devices ourselves + if dominfo._stateGet() != DOM_STATE_HALTED: + dominfo.image.signalDeviceModel("snapshot-delete", + "snapshot-deleted", name) + else: + for dev_type, dev_info in dominfo.info.all_devices_sxpr(): + if dev_type != 'tap': + continue + + # Fetch the filename and strip off tap:xyz: + image_file = sxp.child_value(dev_info, 'uname') + image_file = image_file.split(':')[2] + + os.system("qemu-img-xen snapshot -d %s %s" % + (name, image_file)); + + + os.unlink(snap_file) + def domain_pincpu(self, domid, vcpu, cpumap): """Set which cpus vcpu can use Index: xen-4.1.2-testing/tools/python/xen/xm/main.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xm/main.py +++ xen-4.1.2-testing/tools/python/xen/xm/main.py @@ -123,6 +123,14 @@ SUBCOMMAND_HELP = { 'Restore a domain from a saved state.'), 'save' : ('[-c|-f] <Domain> <CheckpointFile>', 'Save a domain state to restore later.'), + 'snapshot-create' : ('[-d] <Domain> <SnapshotName>', + 'Snapshot a running domain.'), + 'snapshot-list' : ('<Domain>', + 'List available snapshots for a domain.'), + 'snapshot-apply' : ('<Domain> <SnapshotName>', + 'Apply previous snapshot to domain.'), + 'snapshot-delete' : ('<Domain> <SnapshotName>', + 'Delete snapshot of domain.'), 'shutdown' : ('<Domain> [-waRH]', 'Shutdown a domain.'), 'top' : ('', 'Monitor a host and the domains in real time.'), 'unpause' : ('<Domain>', 'Unpause a paused domain.'), @@ -344,6 +352,9 @@ SUBCOMMAND_OPTIONS = { ('-c', '--checkpoint', 'Leave domain running after creating snapshot'), ('-f', '--force', 'Force to overwrite exist file'), ), + 'snapshot-create': ( + ('-d', '--diskonly', 'Perform disk only snapshot of domain'), + ), 'restore': ( ('-p', '--paused', 'Do not unpause domain after restoring it'), ), @@ -395,6 +406,10 @@ common_commands = [ "restore", "resume", "save", + "snapshot-create", + "snapshot-list", + "snapshot-apply", + "snapshot-delete", "shell", "shutdown", "start", @@ -429,6 +444,10 @@ domain_commands = [ "restore", "resume", "save", + "snapshot-create", + "snapshot-list", + "snapshot-apply", + "snapshot-delete", "shutdown", "start", "suspend", @@ -863,6 +882,62 @@ def xm_event_monitor(args): # ######################################################################### +def xm_snapshot_create(args): + + arg_check(args, "snapshot-create", 2, 3) + + try: + (options, params) = getopt.gnu_getopt(args, 'd', ['diskonly']) + except getopt.GetoptError, opterr: + err(opterr) + sys.exit(1) + + diskonly = False + for (k, v) in options: + if k in ['-d', '--diskonly']: + diskonly = True + + if len(params) != 2: + err("Wrong number of parameters") + usage('snapshot-create') + + if serverType == SERVER_XEN_API: + server.xenapi.VM.snapshot_create(get_single_vm(params[0]), params[1], diskonly) + else: + server.xend.domain.snapshot_create(params[0], params[1], diskonly) + +def xm_snapshot_list(args): + arg_check(args, "snapshot-list", 1, 2) + + snapshots = None + if serverType == SERVER_XEN_API: + snapshots = server.xenapi.VM.snapshot_list(get_single_vm(args[0])) + else: + snapshots = server.xend.domain.snapshot_list(args[0]) + + if snapshots: + print "Available snapshots for domain %s" % args[0] + for snapshot in snapshots: + print " %s" % snapshot + else: + print "No snapshot available for domain %s" % args[0] + +def xm_snapshot_apply(args): + arg_check(args, "snapshot-apply", 2, 3) + + if serverType == SERVER_XEN_API: + server.xenapi.VM.snapshot_apply(get_single_vm(args[0]), args[1]) + else: + server.xend.domain.snapshot_apply(args[0], args[1]) + +def xm_snapshot_delete(args): + arg_check(args, "snapshot-delete", 2, 3) + + if serverType == SERVER_XEN_API: + server.xenapi.VM.snapshot_delete(get_single_vm(args[0]), args[1]) + else: + server.xend.domain.snapshot_delete(args[0], args[1]) + def xm_save(args): arg_check(args, "save", 2, 4) @@ -3827,6 +3902,10 @@ commands = { "restore": xm_restore, "resume": xm_resume, "save": xm_save, + "snapshot-create": xm_snapshot_create, + "snapshot-list": xm_snapshot_list, + "snapshot-apply": xm_snapshot_apply, + "snapshot-delete": xm_snapshot_delete, "shutdown": xm_shutdown, "start": xm_start, "sysrq": xm_sysrq, Index: xen-4.1.2-testing/tools/python/xen/xend/XendDomainInfo.py =================================================================== --- xen-4.1.2-testing.orig/tools/python/xen/xend/XendDomainInfo.py +++ xen-4.1.2-testing/tools/python/xen/xend/XendDomainInfo.py @@ -508,8 +508,6 @@ class XendDomainInfo: self._setSchedParams() self._storeVmDetails() self._createChannels() - self._createDevices() - self._storeDomDetails() self._endRestore() except: log.exception('VM resume failed') @@ -2371,7 +2369,7 @@ class XendDomainInfo: return self.getDeviceController(deviceClass).reconfigureDevice( devid, devconfig) - def _createDevices(self): + def _createDevices(self, resume = False): """Create the devices for a vm. @raise: VmError for invalid devices @@ -2420,7 +2418,7 @@ class XendDomainInfo: if self.image: - self.image.createDeviceModel() + self.image.createDeviceModel(resume) #if have pass-through devs, need the virtual pci slots info from qemu self.pci_device_configure_boot() @@ -3046,7 +3044,7 @@ class XendDomainInfo: self._introduceDomain() self.image = image.create(self, self.info) if self.image: - self.image.createDeviceModel(True) + self._createDevices(True) self._storeDomDetails() self._registerWatches() self.refreshShutdown()
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor