Skip to content

Commit ae9f491

Browse files
committed
Add support for snapshots
1 parent 5e64767 commit ae9f491

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3009
-165
lines changed

src/packaging/bin/spreaper

+114
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,33 @@ def _arguments_for_list_runs(parser):
278278
parser.add_argument("cluster", nargs='?', default=None, help="the cluster name")
279279
parser.add_argument("keyspace", nargs='?', default=None, help="the keyspace name")
280280

281+
def _arguments_for_list_snapshots(parser):
282+
"""Arguments needed to list snapshots"""
283+
parser.add_argument("cluster", nargs='?', default=None, help="the cluster name")
284+
parser.add_argument("--node", default=None,
285+
help=("A single node to get the snapshot list from"))
286+
287+
288+
def _arguments_for_create_snapshots(parser):
289+
"""Arguments needed to create snapshots"""
290+
parser.add_argument("cluster_name", nargs='?', default=None, help="the cluster name")
291+
parser.add_argument("--node", default=None,
292+
help=("A single node to get the snapshot list from"))
293+
parser.add_argument("--keyspace", default=None,
294+
help=("A single keyspace to snapshot"))
295+
parser.add_argument("--name", default="reaper",
296+
help=("Name that will prefix the snapshot"))
297+
_argument_owner(parser)
298+
_argument_cause(parser)
299+
300+
def _arguments_for_delete_snapshots(parser):
301+
"""Arguments needed to delete snapshots"""
302+
parser.add_argument("cluster_name", nargs='?', default=None, help="the cluster name")
303+
parser.add_argument("snapshot_name", nargs='?', default=None, help="the snapshot name")
304+
parser.add_argument("--node", default=None,
305+
help=("A single node to get the snapshot list from"))
306+
307+
281308

282309
def _parse_arguments(command, description, usage=None, extra_arguments=None):
283310
"""Generic argument parsing done by every command"""
@@ -330,6 +357,9 @@ Usage: spreaper [<global_args>] <command> [<command_args>]
330357
pause-schedule Pause a repair schedule.
331358
delete-schedule Delete a repair schedule.
332359
ping Test connectivity to the Reaper service.
360+
list-snapshots List all snapshots for a given cluster or node.
361+
take-snapshot Take a snapshot for a whole cluster or a specific node.
362+
delete-snapshot Delete a named snapshot on a whole cluster or a specific node.
333363
"""
334364

335365

@@ -428,6 +458,90 @@ class ReaperCLI(object):
428458
printq("# Found {0} schedules:".format(len(data)))
429459
print json.dumps(data, indent=2, sort_keys=True)
430460

461+
def list_snapshots(self):
462+
reaper, args = ReaperCLI.prepare_reaper(
463+
"list-snapshots",
464+
"List snapshots for a given cluster or node",
465+
extra_arguments=_arguments_for_list_snapshots
466+
)
467+
if not args.node:
468+
printq("# Listing snapshots for cluster '{0}'".format(args.cluster))
469+
snapshots = json.loads(reaper.get("snapshot/cluster/{0}".format(args.cluster)))
470+
printq("# Found {0} snapshots".format(len(snapshots)))
471+
print json.dumps(snapshots, indent=2, sort_keys=True)
472+
else:
473+
printq("# Listing snapshots for cluster '{0}' and node '{1}'".format(args.cluster, args.node))
474+
snapshots = json.loads(reaper.get("snapshot/{0}/{1}".format(args.cluster, args.node)))
475+
printq("# Found {0} snapshots".format(len(snapshots)))
476+
print json.dumps(snapshots, indent=2, sort_keys=True)
477+
478+
def take_snapshot(self):
479+
reaper, args = ReaperCLI.prepare_reaper(
480+
"take-snapshot",
481+
"Take a snapshot. You need to register a cluster "
482+
"into Reaper (add-cluster) before calling this.",
483+
extra_arguments=_arguments_for_create_snapshots
484+
)
485+
if not args.cluster_name:
486+
print("# Please specify a cluster")
487+
exit(1)
488+
if args.keyspace:
489+
if args.node:
490+
print ("# Taking a snapshot on cluster '{0}', node '{1}' and keyspace '{2}', "
491+
).format(args.cluster_name, args.node, args.keyspace)
492+
reply = reaper.post("snapshot/{0}/{1}".format(args.cluster_name, args.node),
493+
keyspace=args.keyspace,
494+
owner=args.owner, cause=args.cause,
495+
snapshot_name=args.name)
496+
else:
497+
print ("# Taking a snapshot on cluster '{0}' and keyspace '{1}', "
498+
).format(args.cluster_name, args.keyspace)
499+
reply = reaper.post("snapshot/cluster/{0}".format(args.cluster_name),
500+
keyspace=args.keyspace,
501+
owner=args.owner, cause=args.cause,
502+
snapshot_name=args.name)
503+
else:
504+
if args.node:
505+
print ("# Taking a snapshot on cluster '{0}' and node '{1}', "
506+
).format(args.cluster_name, args.node)
507+
reply = reaper.post("snapshot/{0}/{1}".format(args.cluster_name, args.node),
508+
keyspace=args.keyspace,
509+
owner=args.owner, cause=args.cause,
510+
snapshot_name=args.name)
511+
else:
512+
print ("# Taking a snapshot on cluster '{0}', "
513+
).format(args.cluster_name)
514+
reply = reaper.post("snapshot/cluster/{0}".format(args.cluster_name),
515+
keyspace=args.keyspace,
516+
owner=args.owner, cause=args.cause,
517+
snapshot_name=args.name)
518+
519+
snapshot = json.loads(reply)
520+
printq("# Snapshot taken")
521+
print json.dumps(snapshot, indent=2, sort_keys=True)
522+
523+
def delete_snapshot(self):
524+
reaper, args = ReaperCLI.prepare_reaper(
525+
"delete-snapshot",
526+
"Delete a snapshot.",
527+
extra_arguments=_arguments_for_delete_snapshots
528+
)
529+
if not args.cluster_name or not args.snapshot_name:
530+
print("# Please specify a cluster and a snapshot name")
531+
exit(1)
532+
533+
if args.node:
534+
print ("# Deleting snapshot '{1}' on cluster '{0}' and node '{2}'"
535+
).format(args.snapshot_name, args.cluster_name, args.node)
536+
reply = reaper.delete("snapshot/{0}/{1}/{2}".format(args.cluster_name, args.node, args.snapshot_name))
537+
else:
538+
print ("# Deleting snapshot '{1}' on cluster '{0}', "
539+
).format(args.cluster_name, args.snapshot_name)
540+
reply = reaper.delete("snapshot/cluster/{0}/{1}".format(args.cluster_name, args.snapshot_name))
541+
542+
printq("# Snapshot deleted : {0}".format(reply))
543+
544+
431545
def status_cluster(self):
432546
reaper, args = ReaperCLI.prepare_reaper(
433547
"status-cluster",

src/server/src/main/java/io/cassandrareaper/AppContext.java

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import io.cassandrareaper.jmx.JmxConnectionFactory;
1818
import io.cassandrareaper.service.RepairManager;
19+
import io.cassandrareaper.service.SnapshotManager;
1920
import io.cassandrareaper.storage.IStorage;
2021

2122
import java.net.InetAddress;
@@ -44,6 +45,7 @@ public final class AppContext {
4445
public JmxConnectionFactory jmxConnectionFactory;
4546
public ReaperApplicationConfiguration config;
4647
public MetricRegistry metricRegistry = new MetricRegistry();
48+
public SnapshotManager snapshotManager;
4749

4850
private static String initialiseInstanceAddress() {
4951
String reaperInstanceAddress;

src/server/src/main/java/io/cassandrareaper/ReaperApplication.java

+8
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import io.cassandrareaper.resources.ReaperHealthCheck;
2323
import io.cassandrareaper.resources.RepairRunResource;
2424
import io.cassandrareaper.resources.RepairScheduleResource;
25+
import io.cassandrareaper.resources.SnapshotResource;
2526
import io.cassandrareaper.service.AutoSchedulingManager;
2627
import io.cassandrareaper.service.RepairManager;
2728
import io.cassandrareaper.service.SchedulingManager;
29+
import io.cassandrareaper.service.SnapshotManager;
2830
import io.cassandrareaper.storage.CassandraStorage;
2931
import io.cassandrareaper.storage.IDistributedStorage;
3032
import io.cassandrareaper.storage.IStorage;
@@ -126,6 +128,8 @@ public void run(ReaperApplicationConfiguration config, Environment environment)
126128
.addServlet("prometheusMetrics", new MetricsServlet(CollectorRegistry.defaultRegistry))
127129
.addMapping("/prometheusMetrics");
128130

131+
context.snapshotManager = SnapshotManager.create(context);
132+
129133
LOG.info("initializing runner thread pool with {} threads", config.getRepairRunThreadCount());
130134
context.repairManager = RepairManager.create(context);
131135
context.repairManager.initializeThreadPool(
@@ -197,6 +201,10 @@ public void run(ReaperApplicationConfiguration config, Environment environment)
197201

198202
final RepairScheduleResource addRepairScheduleResource = new RepairScheduleResource(context);
199203
environment.jersey().register(addRepairScheduleResource);
204+
205+
final SnapshotResource snapshotResource = new SnapshotResource(context);
206+
environment.jersey().register(snapshotResource);
207+
200208
Thread.sleep(1000);
201209

202210
SchedulingManager.start(context);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package io.cassandrareaper.core;
16+
17+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
18+
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
19+
import com.google.common.base.Optional;
20+
import org.joda.time.DateTime;
21+
22+
@JsonDeserialize(builder = Snapshot.Builder.class)
23+
public final class Snapshot {
24+
private String host;
25+
private String name;
26+
private String keyspace;
27+
private String table;
28+
private Double trueSize;
29+
private Double sizeOnDisk;
30+
private Optional<String> owner;
31+
private Optional<String> cause;
32+
private Optional<DateTime> creationDate;
33+
private Optional<String> clusterName;
34+
35+
private Snapshot(Builder builder) {
36+
this.name = builder.name;
37+
this.host = builder.host;
38+
this.keyspace = builder.keyspace;
39+
this.table = builder.table;
40+
this.trueSize = builder.trueSize;
41+
this.sizeOnDisk = builder.sizeOnDisk;
42+
this.owner = Optional.fromNullable(builder.owner);
43+
this.cause = Optional.fromNullable(builder.cause);
44+
this.creationDate = Optional.fromNullable(builder.creationDate);
45+
this.clusterName = Optional.fromNullable(builder.clusterName);
46+
}
47+
48+
public String getName() {
49+
return name;
50+
}
51+
52+
public String getHost() {
53+
return host;
54+
}
55+
56+
public String getKeyspace() {
57+
return keyspace;
58+
}
59+
60+
public String getTable() {
61+
return table;
62+
}
63+
64+
public Double getTrueSize() {
65+
return trueSize;
66+
}
67+
68+
public Double getSizeOnDisk() {
69+
return sizeOnDisk;
70+
}
71+
72+
public Optional<String> getOwner() {
73+
return owner;
74+
}
75+
76+
public Optional<String> getCause() {
77+
return cause;
78+
}
79+
80+
public Optional<DateTime> getCreationDate() {
81+
return creationDate;
82+
}
83+
84+
public String getClusterName() {
85+
return clusterName.or("");
86+
}
87+
88+
public String toString() {
89+
return "{name="
90+
+ name
91+
+ ", host="
92+
+ host
93+
+ ", keyspace="
94+
+ keyspace
95+
+ ", table="
96+
+ table
97+
+ ", true size="
98+
+ trueSize
99+
+ ", size on disk="
100+
+ sizeOnDisk
101+
+ "}";
102+
}
103+
104+
/**
105+
* Creates builder to build {@link Snapshot}.
106+
*
107+
* @return created builder
108+
*/
109+
public static Builder builder() {
110+
return new Builder();
111+
}
112+
113+
@JsonPOJOBuilder(buildMethodName = "build", withPrefix = "with")
114+
public static final class Builder {
115+
private String name;
116+
private String host;
117+
private String keyspace;
118+
private String table;
119+
private Double trueSize;
120+
private Double sizeOnDisk;
121+
private String owner;
122+
private String cause;
123+
private DateTime creationDate;
124+
private String clusterName;
125+
126+
private Builder() {}
127+
128+
public Builder withName(String name) {
129+
this.name = name;
130+
return this;
131+
}
132+
133+
public Builder withHost(String host) {
134+
this.host = host;
135+
return this;
136+
}
137+
138+
public Builder withKeyspace(String keyspace) {
139+
this.keyspace = keyspace;
140+
return this;
141+
}
142+
143+
public Builder withTable(String table) {
144+
this.table = table;
145+
return this;
146+
}
147+
148+
public Builder withTrueSize(Double trueSize) {
149+
this.trueSize = trueSize;
150+
return this;
151+
}
152+
153+
public Builder withSizeOnDisk(Double sizeOnDisk) {
154+
this.sizeOnDisk = sizeOnDisk;
155+
return this;
156+
}
157+
158+
public Builder withCause(String cause) {
159+
this.cause = cause;
160+
return this;
161+
}
162+
163+
public Builder withOwner(String owner) {
164+
this.owner = owner;
165+
return this;
166+
}
167+
168+
public Builder withClusterName(String clusterName) {
169+
this.clusterName = clusterName;
170+
return this;
171+
}
172+
173+
public Builder withCreationDate(DateTime creationDate) {
174+
this.creationDate = creationDate;
175+
return this;
176+
}
177+
178+
public Snapshot build() {
179+
return new Snapshot(this);
180+
}
181+
}
182+
}

src/server/src/main/java/io/cassandrareaper/jmx/JmxConnectionFactory.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ protected JmxProxy connect(
118118
}
119119
}
120120

121-
public final JmxProxy connect(Node node, int connectionTimeout)
121+
public JmxProxy connect(Node node, int connectionTimeout)
122122
throws ReaperException, InterruptedException {
123123
return connect(Optional.<RepairStatusHandler>absent(), node, connectionTimeout);
124124
}
@@ -154,7 +154,7 @@ public final JmxProxy connectAny(
154154
throw new ReaperException("no host could be reached through JMX");
155155
}
156156

157-
public final JmxProxy connectAny(Cluster cluster, int connectionTimeout) throws ReaperException {
157+
public JmxProxy connectAny(Cluster cluster, int connectionTimeout) throws ReaperException {
158158
Set<Node> nodes =
159159
cluster
160160
.getSeedHosts()

src/server/src/main/java/io/cassandrareaper/jmx/JmxConnectionsInitializer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private Callable<Optional<String>> connectToJmx(Cluster cluster, List<String> en
8484
return Optional.of(endpoints.get(0));
8585

8686
} catch (RuntimeException e) {
87-
LOG.debug("failed to connect to hosts {} through JMX", endpoints.get(0), e);
87+
LOG.info("failed to connect to hosts {} through JMX", endpoints.get(0), e);
8888
return Optional.absent();
8989
}
9090
};

0 commit comments

Comments
 (0)