Merge pull request #11 from ansible-semaphore/feature/ui-work

UI Work
This commit is contained in:
Alan Campbell 2015-03-25 09:27:47 -04:00
commit 0cc72360be
19 changed files with 165 additions and 106 deletions

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM iojs:onbuild
ENV NODE_ENV production
ADD . /srv/semaphore
WORKDIR /srv/semaphore
RUN rm -f node_modules/
RUN npm install
CMD ["node", "/srv/semaphore/bin/semaphore"]
EXPOSE 80

100
README.md
View File

@ -1,49 +1,81 @@
semaphore # semaphore
=========
Open Source Alternative to Ansible Tower Open Source Alternative to Ansible Tower
![](public/img/screenshot.png) ![screenshot](public/img/screenshot.png)
Features ## Features
--------
The basics of Ansible Tower, but in addition: The basics of Ansible Tower, but in addition:
- Fast, Simple interface that doesnt get in the way - [x] Fast, Simple interface (not having to submit a million forms to get something simple done)
- Task output is streamed live via websocket - [x] Task output is streamed live via websocket
- Free. MIT Licensed. Do what you want. - [x] Create inventories per playbook
- [x] Add rsa keys (to authenticate git repositories)
- [x] Run playbooks against specified hosts
- [ ] Multiple Users support
How to run: ## Docker quickstart
-----------
1. Install Vagrant ### Run redis
2. Run `vagrant up`
3. Open [localhost:3000](http://localhost:3000)
Development steps: ```
docker run -d \
--name=redisio \
--restart=always \
-v /var/lib/redisio:/var/lib/redis \
-p 127.0.0.1:6379:6379 \
castawaylabs/redis-docker
```
Install requirements: ### Run mongodb
- node.js >= 0.11.x
- an isolated environment (e.g. Docker / NodeGear)
- ansible (the tool)
- mongodb & redis
- Sudo access (this might change). To run jobs, this tool writes private keys to /root/.ssh and copies playbook directories to /root/.
1. Copy `lib/credentials.default.json` to `lib/credentials.json` and customise, or export relevant environment variables ```
2. `bower install` docker run -d \
3. `node bin/semaphore` --name=mongodb \
--restart=always \
-v /var/lib/mongodb:/var/lib/mongodb \
-p 127.0.0.1:27017:27017 \
castawaylabs/mongodb-docker
```
Initial Login ### Run semaphore
-------------
```
docker run -d \
--name=semaphore \
--restart=always \
--link redisio:redis \
--link mongodb:mongo \
-e MONGODB_URL="mongodb://mongo/semaphore" \
-e REDIS_HOST="redis" \
-p 80:80 \
castawaylabs/semaphore
```
## Development
1. Install VirtualBox & Vagrant
2. Run `vagrant plugin install gatling-rsync-auto`
3. Run `vagrant up` to start the vagrant box
4. Run `vagrant gatling-rsync-auto` to synchronise changes from your local machine to vagrant
### Running semaphore inside vagrant
1. `vagrant ssh`, `cd /opt/semaphore`
2. `npm install`
3. `bower install`
4. `npm install -g nodemon`
5. `nodemon bin/semaphore`
## Initial Login
``` ```
Email: 'admin@semaphore.local' Email: 'admin@semaphore.local'
Password: 'CastawayLabs' Password: 'CastawayLabs'
``` ```
Environment Variables ## Environment Variables
---------------------
Use these variables to override the config. Use these variables to override the config.
@ -58,9 +90,19 @@ Use these variables to override the config.
| SMTP_PASS | Mandrill smtp password | | | SMTP_PASS | Mandrill smtp password | |
| MONGODB_URL | Mongodb URL | `mongodb://127.0.0.1/semaphore` | | MONGODB_URL | Mongodb URL | `mongodb://127.0.0.1/semaphore` |
Note to Ansible guys ## Vision and goals for v1
--------------------
- Be able to specify environment information per playbook / per task
- Schedule jobs
- Email alerts
- Multiple user support
## Note to Ansible guys
> Thanks very much for making Ansible, and Ansible Tower. It is a great tool!. Your UI is pretty horrible though, and so we'd be happy if you could learn and use parts of this tool in your Tower. > Thanks very much for making Ansible, and Ansible Tower. It is a great tool!. Your UI is pretty horrible though, and so we'd be happy if you could learn and use parts of this tool in your Tower.
It would be amazing if this could be your `Community Edition` of Ansible Tower. It would be amazing if this could be your `Community Edition` of Ansible Tower.
## License
MIT

View File

@ -13,4 +13,4 @@
"angular-couch-potato": "~0.1.1", "angular-couch-potato": "~0.1.1",
"angular-ui-router": "~0.2.10" "angular-ui-router": "~0.2.10"
} }
} }

View File

@ -4,6 +4,9 @@ var express = require('express')
exports.unauthorized = function (app, template) { exports.unauthorized = function (app, template) {
template([ template([
'tasks',
'jobs',
'hosts',
'view' 'view'
], { ], {
prefix: 'playbook' prefix: 'playbook'

View File

@ -27,19 +27,12 @@ html
li: a(ui-sref="addPlaybook") Add Playbook li: a(ui-sref="addPlaybook") Add Playbook
.container-fluid .container-fluid
.row .col-lg-12
.col-sm-3.col-lg-2 block content
ul.nav ui-view(autoscroll="false")
h2.no-top-margin Playbooks p.lead.text-center
li(ng-repeat="playbook in playbooks") i.fa.fa-spin.fa-cog
a(ui-sref="playbook.view({ playbook_id: playbook._id })") {{ playbook.name }} | Loading...
.col-sm-9.col-lg-10
block content
ui-view(autoscroll="false")
p.lead.text-center
i.fa.fa-spin.fa-cog
| Loading...
block js block js
script(src="/vendor/requirejs/require.js" data-main="/js/semaphore.js") script(src="/vendor/requirejs/require.js" data-main="/js/semaphore.js")

View File

@ -1,7 +1,9 @@
h1 Add Playbook h1
span(ng-if="!playbook.data._id") Add Playbook
span(ng-if="playbook.data._id.length > 0") Edit Playbook
.row .row
.col-md-6 div(ng-class="{ 'col-md-6': !playbook.data._id, 'col-md-12': playbook.data._id.length > 0 }")
.panel.panel-default .panel.panel-default
.panel-body .panel-body
form.form-horizontal(name="playbookForm") form.form-horizontal(name="playbookForm")
@ -31,5 +33,6 @@ h1 Add Playbook
button.btn.btn-success(ng-click="add()" ng-disabled="playbookForm.$invalid") button.btn.btn-success(ng-click="add()" ng-disabled="playbookForm.$invalid")
span(ng-if="!playbook.data._id") Add span(ng-if="!playbook.data._id") Add
span(ng-if="playbook.data._id.length > 0") Edit span(ng-if="playbook.data._id.length > 0") Edit
.col-md-6 a.btn.btn-danger(ng-if="playbook.data._id.length > 0" ng-click="delete()" style="margin-left: 10px;") Delete
.col-md-6(ng-if="!playbook.data._id")
p.lead It is recommended you create an identity before creating playbooks. p.lead It is recommended you create an identity before creating playbooks.

View File

@ -1,4 +1,4 @@
h2 Hosts h1 Hosts
button.btn.btn-default.pull-right(ng-click="add()") Add Group button.btn.btn-default.pull-right(ng-click="add()") Add Group
div(ng-repeat="hostgroup in hostgroups.hostgroups") div(ng-repeat="hostgroup in hostgroups.hostgroups")

View File

@ -1,4 +1,4 @@
h2 Jobs h1 Jobs
button.btn.btn-default.pull-right(ng-click="add()") Add Job button.btn.btn-default.pull-right(ng-click="add()") Add Job
table.table.table-hover table.table.table-hover

View File

@ -5,6 +5,6 @@ h1 Playbooks
table.table table.table
tr(ng-repeat="playbook in playbooks") tr(ng-repeat="playbook in playbooks")
td: a(ui-sref="playbook.view({ playbook_id: playbook._id })") {{ playbook.name }} td: a(ui-sref="playbook.tasks({ playbook_id: playbook._id })") {{ playbook.name }}
blockquote Playbooks are ansible playbooks. Each playbook defines a git repository with the playbook code. Semaphore downloads the playbook and runs the task file. blockquote Playbooks are ansible playbooks. Each playbook defines a git repository with the playbook code. Semaphore downloads the playbook and runs the task file.

View File

@ -1,7 +1,7 @@
h2 Tasks h1(ng-if="tasks.tasks.length") Tasks
small(ng-if="status.length > 0" ng-bind="status") small(ng-if="status.length > 0" ng-bind="status")
table.table.table-hover table.table.table-hover(ng-if="tasks.tasks.length")
thead thead
tr tr
th Job Name th Job Name
@ -33,4 +33,8 @@ table.table.table-hover
button(data-dismiss="modal").close: span × button(data-dismiss="modal").close: span ×
h4.modal-title Task Output h4.modal-title Task Output
.modal-body .modal-body
pre: code {{ openTask.data.output }} pre: code {{ openTask.data.output }}
h4.text-center.text-muted(ng-if="!tasks.tasks.length" style="margin-top: 15px;") It seems no 
a(ui-sref="playbook.jobs") jobs
|  have been run yet.

View File

@ -1,16 +1,15 @@
h1 {{ playbook.data.name }} mixin menuItem(state, name)
.btn-group.pull-right li(ng-class="{ active: $state.includes('#{state}') }"): a(ui-sref=state+"({ playbook_id: playbook.data._id })")= name
button.btn.btn-success(ui-sref="playbook.edit({ playbook_id: playbook.data._id })") Edit
button.btn.btn-danger(ng-click="delete()") Delete
hr .row
.col-sm-3.col-lg-2
ui-view(name="tasks") ul.nav
h2.no-top-margin {{ playbook.data.name }}
hr +menuItem("playbook.tasks", "Tasks")
+menuItem("playbook.jobs", "Jobs")
ui-view(name="jobs") +menuItem("playbook.hosts", "Hosts")
br
hr ul.nav
li: a(ui-sref="playbook.edit") Edit
ui-view(name="hosts") .col-sm-9.col-lg-10
ui-view

View File

@ -22,7 +22,6 @@
- runit - runit
- npm: name={{ item }} global=yes - npm: name={{ item }} global=yes
with_items: with_items:
- grunt-cli
- bower - bower
# source is copied using ansible. # source is copied using ansible.

View File

@ -28,4 +28,10 @@ h1, h2, h3, h4, h5, h6 {
&.no-top-margin { &.no-top-margin {
margin-top: 0; margin-top: 0;
} }
}
ul.nav {
& > li.active a {
background-color: #eee;
}
} }

View File

@ -18,7 +18,7 @@ define([
$scope.playbook.add() $scope.playbook.add()
.success(function (data) { .success(function (data) {
playbooks.getPlaybooks(function () { playbooks.getPlaybooks(function () {
$state.transitionTo('playbook.view', { $state.transitionTo('playbook.tasks', {
playbook_id: data._id playbook_id: data._id
}); });
}); });

View File

@ -5,10 +5,10 @@ define([
'factories/hostgroup', 'factories/hostgroup',
'factories/host' 'factories/host'
], function(app, $) { ], function(app, $) {
app.registerController('HostsCtrl', ['$scope', '$state', 'hostgroups', 'Host', function($scope, $state, hostgroups, Host) { app.registerController('PlaybookHostsCtrl', ['$scope', '$state', 'hostgroups', 'Host', function($scope, $state, hostgroups, Host) {
$scope.hostgroups = hostgroups; $scope.hostgroups = hostgroups;
hostgroups.get($scope.playbook, function () { hostgroups.get($scope.playbook, function () {
}); });

View File

@ -4,9 +4,9 @@ define([
'services/jobs', 'services/jobs',
'factories/job' 'factories/job'
], function(app, $) { ], function(app, $) {
app.registerController('JobsCtrl', ['$scope', 'jobs', function($scope, jobs) { app.registerController('PlaybookJobsCtrl', ['$scope', 'jobs', function($scope, jobs) {
$scope.jobs = jobs; $scope.jobs = jobs;
jobs.get($scope.playbook, function () { jobs.get($scope.playbook, function () {
}); });

View File

@ -7,9 +7,9 @@ define([
], function(app, $, io) { ], function(app, $, io) {
var socket = io(); var socket = io();
app.registerController('TasksCtrl', ['$scope', 'tasks', 'Task', function($scope, tasks, Task) { app.registerController('PlaybookTasksCtrl', ['$scope', 'tasks', 'Task', function($scope, tasks, Task) {
$scope.tasks = tasks; $scope.tasks = tasks;
tasks.get($scope.playbook, function () { tasks.get($scope.playbook, function () {
}); });
@ -40,7 +40,7 @@ define([
if (!task.data.output) { if (!task.data.output) {
task.data.output = ""; task.data.output = "";
} }
task.data.output += data.output; task.data.output += data.output;
if (!$scope.$$phase) { if (!$scope.$$phase) {

View File

@ -30,10 +30,7 @@ define([
controller: 'PlaybookCtrl', controller: 'PlaybookCtrl',
templateUrl: '/view/playbook/view', templateUrl: '/view/playbook/view',
resolve: { resolve: {
dummy: $couchPotatoProvider.resolve(['controllers/playbook/playbook', dummy: $couchPotatoProvider.resolve(['controllers/playbook/playbook']),
'controllers/host/hosts',
'controllers/job/jobs',
'controllers/task/tasks']),
playbook: function (Playbook, $stateParams, $q, $state) { playbook: function (Playbook, $stateParams, $q, $state) {
var deferred = $q.defer(); var deferred = $q.defer();
@ -51,36 +48,37 @@ define([
} }
}) })
.state('playbook.view', {
url: '',
views: {
tasks: {
templateUrl: '/view/task/tasks',
controller: 'TasksCtrl'
},
jobs: {
templateUrl: '/view/job/jobs',
controller: 'JobsCtrl'
},
hosts: {
templateUrl: '/view/host/hosts',
controller: 'HostsCtrl'
}
}
})
.state('playbook.edit', { .state('playbook.edit', {
url: '/edit', url: '/edit',
templateUrl: "/view/playbook/add", templateUrl: "/view/playbook/add",
controller: 'EditPlaybookCtrl', controller: 'EditPlaybookCtrl',
resolve: { resolve: {
dummy: $couchPotatoProvider.resolve(['controllers/playbook/edit']) dummy: $couchPotatoProvider.resolve(['controllers/playbook/edit'])
}, }
views: { })
tasks: {
templateUrl: '/view/playbook/add', .state('playbook.tasks', {
controller: 'EditPlaybookCtrl' url: '/tasks',
} templateUrl: "/view/playbook/tasks",
controller: 'PlaybookTasksCtrl',
resolve: {
dummy: $couchPotatoProvider.resolve(['controllers/playbook/tasks'])
}
})
.state('playbook.jobs', {
url: '/jobs',
templateUrl: "/view/playbook/jobs",
controller: 'PlaybookJobsCtrl',
resolve: {
dummy: $couchPotatoProvider.resolve(['controllers/playbook/jobs'])
}
})
.state('playbook.hosts', {
url: '/hosts',
templateUrl: "/view/playbook/hosts",
controller: 'PlaybookHostsCtrl',
resolve: {
dummy: $couchPotatoProvider.resolve(['controllers/playbook/hosts'])
} }
}) })
}) })

View File

@ -1 +0,0 @@
vagrant ssh -- -R 27017:localhost:27017 -R 6379:localhost:6379 -R 3000:localhost:3000